Na ja, eigentlich ist das Thema kein Java Thema – aber hier werde ich Ansätze diskutieren, die Java benutzen.
Warum wollen wir überhaupt verschiedene Datenbanken unterstützen – warum legen wir uns nicht auf eine fest? In meinem Bereich hautpsächlich aus den folgenden zwei Gründen: (1) Entwickler können eine andere (“leichtere”) Datenbank zur Entwicklung benutzen, und eine andere in der Produktion, und (2) Kunden können je nach Bedarf und Preis die Datenbank auswählen (z.B. mysql für kleine Systeme und Oracle für große).
Leider ist die PK-Erzeugung bei kaum einer Datenbank perfekt. Zum einen sind die Mechanismen oft nicht kompatibel – der SQL Dialekt ist zu verschiden. Ausserdem sind fast immer zwei Datenbankoperationen notwendig, obwohl nur eine Operation ausgeführt wird:
- (1) PK erzeugen, und dann (2) Zeile in die Datenbank einsetzen, oder:
- (1) Zeile in die Datenbank einsetzen und dabei den PK generieren, und dann (2) den soeben erzeugten PK aus der Datenkbank holen.
Der zweite Fall kann mit manchen Datenbanken auf eine Operation reduziert werden, wenn Stored Procedures benutzt werden – aber das bindet das System noch stärker an eine bestimmte Datenbank.
Wo kommt der PK her? Wie schon angedeutet, kann der PK beim einsetzen einer Zeile in die Datenbank erzeugt werden – MySQL zum Beispiel hat das AUTO_INCREMENT Keyword, und HSQLDB statt dessen IDENTITY. Der andere übliche Weg ist eine SEQUENCE, aber MySQL kennt diesen Datentyp gar nicht.
Ein weiterer Ansatz, der Kompatibilität einfacher macht, ist eine Sequence-Tabelle – eine Tabelle mit dem einzigen Zweck, PKs zu erzeugen. Und Spring’s DataFieldMaxValueIncrementer Implementierungen nutzen diesen Ansatz. Und nun kommen wir zum eigentlichen Thema dieses Artikels: Wie sollte so eine Sequenz-Tabelle implementiert werden?
Caching
Bevor wir ins Detail der Implementierung gehen, möchte ich auf das Sequence Blocks Entwurfsmuster aufmersam machen. Diese Technik erlaubt es uns, das oben beschriebene Problem der doppelten Datenbankoperation zu beseitigen. Dieses Design Pattern wird von Floyd Marinescu in “EJB Design Patterns” beschrieben.
Bei diesem Pattern wird nicht nur eine, sondern ein Block von PKs aus der Datenbank geholt. Dieser Block wird im Speicher behalten. Wenn diese PKs aufgebraucht sind, wird ein neuer Block geholt. Der einzige Nachteil dieser Technik: Sollte der Prozess, der die PKs haelt, abstürzen, so werden die zu dem Zeitpunkt gehaltenen PKs nie benutzt, es ergibt sich also eine “Lücke” in den benutzten PKs. Für die meisten Anwendungen spielt das allerdings keine Rolle. Spring’s DataFieldMaxValueIncrementer kann dieses Muster benutzen.
Eine Sequenztabelle pro PK?
In der Spring Architektur gibt es “ein Sequenzobjekt pro PK”. Das Sequenzobjekt ist eine Tabelle für MySQL und HSQLDB, und eine Sequenz für DB2, Oracle und PostgreSQL. Das ist ein sicherer und effizienter Ansatz, den so können die Fähigkeiten der benutzten Datenbank voll ausgenutzt werden. Allerdings geht damit wieder ein Stück Portabilität verloren – aber wenigstens ist die Anwendung nach wie vor Unabhängig von der Datenbank, auch wenn der Sequenzgenerator datenbankspezifisch konfiguriert werden muss.
Und hier frage ich mich: Gibt es einen besseren, Datenbank-Unabhängingen Ansatz, den PK zu erzeugen? Wenn uns das gelingen sollte, können wir bei einfachen Anwendungen vielleicht sogar Hibernate benutzen, um die Datenbanktablellen für uns zu erzeugen. Aber das Hauptproblem ist Synchronisation. Wir wollen auf keinen Fall, dass zwei unabhängige Prozesse ausversehen den selben PK erzeugen, z.B. folgendermaßen:
- Prozess 1 erhöht den Zähler
- Prozess 2 erhöht den Zähler
- Prozess 1 ließt den Zähler
- Prozess 2 ließt den Zähler
Selbst wenn wir Transaktionen benutzen, kann dieses Szenario zu doppelten PKs führen (je nachdem, wie Locking implementiert ist, aber darauf wollen können wir uns nicht verlassen). Allerdings können wir explizit die Tabelle (oder besser, Zeile) sperren, und dann wird es funktionieren.
Die letzte Frage ist dann: brauchen wir nach wie vor eine Tabelle pro Sequenz, oder können wir alle Sequenzen mit einer Tabelle bedienen, zum Beispiel so:
+------+-------+ | name | value | +------+-------+ | seq1 | 5 | | seq2 | 383 | +------+-------+
Wenn wir einzelne Zeilen sperren können, ist das ein legitimer Ansatz. Falls allerdings die gesamte Tabelle gesperrt werden muss (bei wenigen Datenbanken der Fall), kann es zu Performanzproblemen kommen, erst recht, wenn das Sequence Block Muster nicht genutzt wird.
Die Praxis
Wie sieht das ganze in der Praxis aus? Ich teste diesen Ansatz im Moment mit einer Hibernate/Spring Implementierung. Der wichtigste Aspekt ist natürlich rigeroses Testen, was gar nicht so einfach ist, da es hier ja um Raceconditions geht. Ich werde diesen Artikel in ein paar Tagen erweitern, sobald ich mit meinem Ansatz zufrieden bin.