Datenbanken und unveränderliche Objekte

English

Ein beliebter Ansatz in der funktionalen Programmierung, aber auch teilweise in der objektorientierten Programmierung ist es, Objekte nach Möglichkeit unveränderlich (engl. „immutable“) zu machen.

Speziell für Applikationen mit Multithreading ist das sehr nützlich, aber grundsätzlich erleichtert es auch im Griff zu haben, wie der Informationsfluss ist und unerwartete Seiteneffekte zu verhindern. Ich will das hier nicht als Dogma postulieren, denn es gibt durchaus legitime Verwendungen von veränderlichen Objekten, aber man sollte sich auf jeden Fall bewusst sein, was das bedeutet und ob sich die Veränderbarkeit (engl. „mutability“) in dem Fall überhaupt lohnt.

Unterscheiden muss man noch die echt unveränderlichen Objekte und diejenigen, die nur durch einen Wrapper oder ein zweites Interface unveränderlich weitergegeben werden, obwohl es noch für einen Teil des Codes Zugriff auf ein Interface gibt, das Veränderungen zulässt. Der typische Fall sind zum Beispiel Collections in Java, die in einem Objekt leben und mit Methoden dieses Objekts verändert werden können, aber die nur als immutable gewrappt herausgegeben werden.

Beispiel:

import java.util.*;

public class A {
    List l = new ArrayList();
    
    public void addStr(String s) {
        l.add(s);
    }

    public List getList() {
        return Collections.unmodifiableList(l);
    }
}

Obwohl also die mit getList() herausgegebenen Liste selbst immutable ist, kann sie mittels add aus A noch verändert werden.

Interessant wird es aber nun, wenn man eine Datenbank ins Spiel bringt.
Datenbank-Inhalte werden in irgendeiner Weise auf Objekte abgebildet. Ob das nun mit Automatismen passiert oder explizit mit Mechanismen wie JDBC, DBI o.ä. und ob es spezifische Objekte oder generische universelle DB-Content-Objekte oder Collections sind, spielt keine so große Rolle, es gibt solche Objekte oder Datenstrukturen.

Nun können sich Datenbankinhalte ändern. Das kann durch die Software, auf die wir uns hier beziehen, selbst passieren, aber auch von außen. Allein das ist schon eine gewisse Komplikation, wenn man es genau anschaut. Die Objekte, mit denen sich die Software befasst, sind dann nämlich schon veraltet. Das kann so akzeptabel sein, wenn man darauf verzichtet, auf Software- oder Frameworkebene ein Caching von solchen Objekten aufzubauen und das Caching allein der Datenbank überlässt, die das im Gegensatz zu Frameworks oder anderer Software, die die Datenbank nur nutzt, problemlos transaktionskonform kann. Schön ist es dann natürlich wieder, wenn die DB netzwerkmäßig nicht zu weit von der Applikation weg ist, sonst ist der Cache leider auf der falschen Seite der Netzwerkverbindung…

Wenn also in der Software die Objekte so kurzlebig sind, dass man ausschließen kann, dass sie veralten, dann kann man sagen, dass sie hinreichend mit dem Stand in der Datenbank übereinstimmen. Mit Transaktionsisolation „serializable“ oder „phantom-read“ und dann innerhalb von einer Transaktion sind diese Objekte, die man in derselben Transaktion gelesen hat, garantiert noch aktuell. Wenn nicht bestimmte OR-mapping-Muster für Collections vorkommen, ist das schon bei „repeatable-read“ der Fall. Man lebt also gut mit „immutable“-Objekten, solange diese nur aus der Datenbank gelesen (SELECT oder READ) werden. Auch das Löschen (DELETE) kann man sich noch vorstellen, aber in diesem Fall muss man schon darauf achten, dass das betreffende Objekt nicht noch an zu vielen Stellen der Software in Gebrauch ist, obwohl es in der Datenbank schon gelöscht ist. Wenn man aber sowieso sicherstellt, dass diese Objekte, die Datenbankinhalte darstellen, nicht länger leben als die entsprechenden Transaktionen, dann sollte auch das handhabbar sein. Auch das Erzeugen neuer Objekte und das Anlegen der entsprechenden Daten in der Datenbank (INSERT oder CREATE) ist möglich.

Schwierig wird es aber mit dem Ändern von Daten in der Datenbank (UPDATE). Bei gängigen OR-Mappings funktioniert das so, dass man das entsprechende vorher gelesen Objekt ändert und dann speichert, in der Hoffnung, dass das Framework die Unterschiede herausfindet und ein entsprechendes Update, eventuell auch eine Kombination aus Updates, Inserts und Deletes initiiert. Das wird mit unveränderbaren (immutable) Objekten schwierig. Man kann auch da Wege finden, aber das geht dann leicht auf Kosten der Eleganz des Frameworks und man hat sich auf jeden Fall damit auseinanderzusetzen, dass die Objekte jetzt auch innerhalb der Transaktion veralten und durch solche mit den aktualisierten Inhalten ersetzt werden müssen.

Die provokante Frage ist, ob man das Update überhaupt braucht. Natürlich braucht man das, aber auch hier kann man genauer schauen, welches die Fälle sind, wo das wirklich so ist und wann andere Ansätze besser sind.
Ein klassisches Beispiel ist eine Software, die irgendwelche Buchungen und irgendwelche Personen verwaltet. Das kann eine Bankensoftware sein, wo man Konten hat. Oder ein Billingsystem einer Telefongesellschaft, wo man die Rechnungen für Kunden generiert und verwaltet. Man hat dort oft so eine Buchungstabelle, in der grundsätzlich nur Datensätze eingefügt werden und niemals Daten gelöscht oder verändert werden, wenn wir einmal Fragen der Archivierung sehr alter Daten ausklammern. Dazu hätte man gerne noch einen Kontostand. Den kann man strenggenommen immer berechnen, indem man einfach alle Buchungen für das Konto aufaddiert. Leider ist die Software dann nachher langsam und für keinerlei praktische Zwecke brauchbar, aber theoretisch sehr schön. Man braucht also schon einen effizienten Mechanismus, um Kontostände zu ermitteln. Vielleicht kann jede Buchung den aus ihr resultierenden Kontostand beinhalten und man muss nur nach der neuesten Buchung suchen. Das ist gefährlich, weil zwei etwa gleichzeitig erzeugte Buchungen sich auf denselben Vorgängerkontostand beziehen. Mit Transaction-Isolation „serializable“ wäre das nicht passiert, aber das bremst natürlich die Datenbank schon sehr aus, ausgerechnet bei der größten Tabelle, in der die ganze Arbeit läuft und wo es sowieso schon den Performance-Flaschenhals gibt. Man kann auch auf Datenbankebene eine Lösung implementieren, etwa mit Triggern oder Materialized Views, wo jedes Insert in der Buchungstabelle ein Update des entsprechenden Kontostands bewirkt. Das hat den Vorteil, dass beliebige DB-Zugriffe von beliebiger Software richtig verarbeitet werden, solange man in der betreffenden Buchungstabelle Delete- und Update-Zugriffe unterbindet. Der Nachteil ist aber, dass die Businesslogik sich jetzt auf die Datenbank und die Applikation verteilt. Das kann man akzeptieren, wenn man das als eine Art abstrakten Constraint ansieht, der von der Datenbank „aktiv“ eingehalten wird. Oder man kann es vermeiden, indem man auf Applikationsebene eine entsprechend Funktionalität zum Einfügen einer Buchung einführt, die den Kontostand anpasst. Wiederum wird das zu falschen Ergebnissen führen, wenn man erlaubt, dass eine zweite Buchung für dasselbe Konto eingefügt wird, während schon eine Transaktion für das Einfügen einer Buchung für das Konto läuft. Man sieht also, dass die richtige Lösung dieser Buchungstabelle nicht einfach ist. Wenn dann noch hinzukommt, dass die Buchungen wiederum zu Transaktionen gruppiert sind, also z.B. die Gutschrift auf einem Konto in derselben Transaktion wie die Abbuchung von einem anderen Konto laufen soll, dann wird es schon interessant, wie man das System so baut, dass es absolut zuverlässig und korrekt ist, auch unter Last, und doch auch performant ist. Und in der Applikation muss man nun mit Konto-Objekten, die den Kontostand enthalten, sehr vorsichtig sein, weil dieser Kontostand veraltet, sobald eine Buchung dazukommt.

Die Tabelle oder die Tabellen mit den Personen, Adressen werden gerne mit Updates aktualisiert, wenn sich Daten ändern. Leute heiraten, ändern die Telefonnummer oder ziehen um. Aber ist das wirklich der richtige Weg? In Wirklichkeit weiß man von dem Umzug schon etwas vorher und hat vielleicht am Umzugstag selbst keine Zeit, das zu erfassen. Und in Wirklichkeit muss das System auch Fragen beantworten können, wie den Namen und die Adresse des Kunden zu der Zeit, als Buchung 23571113a stattfand. Vielleicht kann man Adressänderungen auch als „Buchungen“ speichern, mit einem Gültigkeitsbeginn. Dann wird die Adresse für einen Kunden ermittelt, indem man den Adresseintrag für die Kundennummer sucht, der das größtmögliche Gültigkeitsdatum hat, das gerade noch nicht in der Zukunft liegt. Mit dem Ansatz muss man in dieser Tabelle vielleicht tatsächlich nur neue Datensätze einfügen, wenn sich etwas geändert hat. Und wenn ein Kunde gelöscht wird, dann fügt man einen Datensatz ein, der beinhaltet, dass der Kunde ab einem bestimmten Datum „gelöscht“ ist, aber man kann noch bei Bedarf Daten liefern.

Das Schöne ist nun aber, dass Daten, die in der Datenbank „immutable“ sind, also keine Updates erhalten dürfen, in der Software auch problemlos immutable sein können (und sollten) und dass man sie auch langlebiger als eine Transaktion machen kann. Vorsicht ist aber immer geboten, weil komplexe Objekte in der Applikation sich auch allein durch Inserts in einer Datenbanktabelle ändern würden, wenn etwa eine Collection enthalten ist. Man muss also das OR-Mapping genau kennen, egal wie faszinierend diese Automatismen auch sein mögen. Im Fall von einer Kundenadresse muss man die gültige Adresse immer in Verbindung mit einem Timestamp behandeln, der beim ermitteln der gültigen Adresse eingesetzt wurde. Wenn dieser Timestamp veraltet ist, muss man auch die Adresse neu abfragen, wobei es im Gegensatz zu den Buchungen die Adressänderungen selten auf die Mikrosekunde des Umzugszeitpunkts ankommt, solange man die Konsistenz wahrt und nicht in der Woche des Umzugs wilde Mischungen aus der alten und der neuen Adresse verwendet.

Share Button

Beteilige dich an der Unterhaltung

2 Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*