Blog

Goldener Winterschlaf

Maarten Winkels

Aktualisiert Oktober 23, 2025
8 Minuten

Für unser Projekt verwenden wir Hibernate. Die Anwendung, die wir erstellen, liest Workitems aus der Datenbank, verarbeitet sie (Validierung) und schreibt die Ergebnisse zurück in die Datenbank; eine typische Datenverarbeitungsanwendung. Optimal wäre ein Streaming-Prozess, bei dem ein gigantischer Select verwendet wird, um Millionen von Zeilen abzurufen und jede Zeile in einer Transaktion zu verarbeiten (die Verarbeitung einer Zeile führt zu mehreren DML-Anweisungen). Nun gibt es technische Hindernisse bei der Implementierung der Anwendung auf diese Weise. Das RDBMS sollte in der Lage sein, Millionen von kurzen Transaktionen zu verarbeiten, während die lange Transaktion, die die Zeilen liest, am Leben erhalten wird. Oracle kann dies aufgrund seiner Lesekonsistenzfunktionalität nicht bewältigen. Nach einiger Zeit führt ein ORA-01555: Snapshot zu alt (Rollback-Segment zu klein) unweigerlich zum Absturz der lang laufenden Transaktion. Unsere Implementierung unterteilt den gigantischen Select in kleinere Abschnitte, um zu verhindern, dass die "Snapshots" zu alt werden. Pfff, das erste Hindernis war aus dem Weg geräumt. Nächstes Problem: Hibernate. Wir wählten Hibernate als ORM-Lösung, weil... weil... wir alle bereits damit vertraut waren. Das ist die lahmste Ausrede der Welt und wird meist dazu führen, dass wir das falsche Problem mit dem falschen Tool lösen. Das Problem mit Hibernate in dieser Situation ist gleichzeitig eine seiner Hauptfunktionen. Um transaktionales Write-Behind zu unterstützen, behält Hibernate den Überblick über alle in seine Session geladenen Objekte. Während der Stapelverarbeitung werden alle Arbeitsobjekte in die Session geladen. Der damit verbundene Speicherplatz ist nicht das größte Problem. Jedes Mal, wenn eine Session geleert wird, untersucht Hibernate jedes zugehörige Objekt auf Änderungen und schreibt diese in die Datenbank. Wenn die Sitzung groß wird, nimmt das Flushen immer mehr Zeit in Anspruch, selbst wenn es keine Änderungen gibt, wie es bei dieser langen Lesetransaktion der Fall ist. Die Lösung für dieses Problem ist, die Objekte so schnell wie möglich aus der Sitzung zu entfernen. Da wir die Objekte aus einem ScrollableResults lesen, wird jedes Ergebnis separat geladen. Wir verpacken dieses Ergebnisobjekt in einen EvictingIterator, der jedes Workitem aus der Sitzung evakuieren wird. Bei diesem Ansatz muss man sehr vorsichtig sein, um alle Objekte zu evakuieren, auch die Objekte, die durch Kaskadierung geladen werden. Glücklicherweise ist 'evict' eine Kaskadenoption von Hibernate. Geben Sie also in den Mapping-Dateien cascade='evict' für alle geladenen Assoziationen an und... presto! Gehen wir nun einen Schritt zurück: Welches Problem haben wir hier gelöst? Die Verwendung von Hibernate für den gigantischen Select, um die Objekte aus der Datenbank zu holen, hat uns kein bisschen geholfen. Ganz im Gegenteil, wir müssen die Session von Hibernate umgehen, damit es funktioniert! Genau so kann ein Goldener Hammer Sie in die Irre führen. Anstatt Ihre Probleme zu lösen, führt er zu weiteren Problemen, die lösungsspezifisch sind. Nachdem wir also alles mit Hibernate zum Laufen gebracht hatten, beschlossen wir, etwas Zeit in die Suche nach einer anderen Lösung zu investieren. Unser erster Versuch war Ibatis. Wir beschlossen, eine Woche in den PoC von Ibatis zu investieren. Die meiste Zeit verbrachten wir damit, unsere Konfiguration, den Code und die Tests zu bereinigen, aber schließlich kamen wir zum eigentlichen Ibatis-Code. Das haben wir gefunden:

Der ProofOfConcept... widerlegt! Wir erwarteten einige Verbesserungen durch den Einsatz von Ibatis:

  1. Weniger Code - Keine Umgehung der Session von Hibernate mehr.
  2. Weniger SQL - Die meisten der Abfragen sind SQL-Abfragen, deren Ergebnisse von Hibernate interpretiert werden. Das liegt daran, dass sie Abfragehinweise und Konstrukte enthalten, die nicht einfach durch HQL oder Kriterien wiedergegeben werden können. Dies führt zu einer Menge Doppelarbeit, da einige logische Konstrukte (Joins, Einschränkungen) in mehreren Abfragen verwendet werden. Ibatis ermöglicht die gemeinsame Nutzung von Anweisungsfragmenten.

Wir haben damit begonnen, eine Einfügeanweisung für eines der Objekte zu definieren. Wir verwenden Sequenzen, um den Primärschlüssel in der Datenbank auszufüllen. In Hibernate verwenden wir den HiLoSequenceGenerator, um die Datenbank zu entlasten, wenn wir eine große Menge an Daten einfügen. Wie würden wir dies in Ibatis implementieren? Nun, Ibatis verfügt über selectKey-Eigenschaften, um den Primärschlüssel eines neu eingefügten Objekts zu generieren. Der hi/lo-Mechanismus ist eine sehr einfache Erweiterung, die auf jeden Schlüsselgenerierungsmechanismus angewendet werden kann. Er wendet einfach eine Lineair-Transformation auf den generierten Schlüssel an, was zu einem ganzen Bündel von Schlüsseln führt. Wie können wir dies in Ibatis implementieren? Nun, wir könnten einfach das SelectKeyStatement erweitern. Oke erweitern, aber dann müssen wir in Ibatis unsere eigene Implementierung verwenden und nicht die normale SelectKeyStatement-Implementierung. Wo wird das Objekt also erstellt? Wird das Factory-Muster verwendet? Gibt es ein Plug-in oder einen Erweiterungspunkt? Nein! Das SelectKeyStatement wird direkt beim Lesen der Konfigurationsdatei instanziiert. Das ist ein wenig enttäuschend! Na ja, vielleicht denken wir hier zu sehr an Hibernate. Wir könnten hi/lo einfach vergessen, unsere Sequenzen auf genau den nächsten Wert zurücksetzen, den wir brauchen, und sie dann bei jeder neuen Eingabe verwenden. Wenn wir schon dabei sind, welche Erweiterungspunkte hat Ibatis? Nun, wenn wir uns die Quellen ansehen, finden wir... drei Erweiterungen< im Client! Die Engine verfügt über keine Erweiterungen. Das scheint ein großer Nachteil zu sein. Meiner Erfahrung nach sind ORM-Lösungen nie zu 100 % passend. Es gibt immer diese eine Tabelle/Objekt-Zuordnung, die ein paar Anpassungen erfordert, damit sie richtig funktioniert. Ein Framework, das so begrenzte Möglichkeiten für Erweiterungen bietet, macht sich selbst unbrauchbar. Oke, lassen Sie uns nicht so schnell aufgeben. Wir können immer noch versuchen, wie weit wir mit Ibatis in seiner jetzigen Form kommen können. Wir müssen nur eine Handvoll verschiedener SQL-Abfragen in der Datenbank ausführen und das Ergebnis jeder Abfrage sollte auf einige wenige Objekte abgebildet werden. Das sollte möglich sein. Lassen Sie uns einen Test schreiben, der eine Sequenz verwendet und dann ein Objekt einfügt. Wir verwenden HSQLDB für unsere Tests. Es ist schnell, einfach zu bedienen und kann auf allen Entwickler- und Testplattformen ausgeführt werden. Jetzt müssen wir also eine "insert"-Anweisung in unserer SQL-Map-Konfiguration erstellen. Die Einfügeanweisung ist einfach; es handelt sich um Standard-SQL: [sql]INSERT INTO TABLE (COL1, COL2, COL3, ...) VALUES (?, ?, ?, ?, ...)[/sql] Dies funktioniert auf jedem gesetzestreuen Datenbanksystem. Nun zu der "selectKey"-Anweisung... Bei Oracle sieht sie etwa so aus [sql]SELECT SEQUENCE.NEXTVAL FROM DUAL[/sql] Bei HSQLDB ist das ziemlich vage. Ein Wert aus einer Sequenz kann nur in einer normalen "SELECT"-Anweisung ausgewählt werden. Die Anweisung sollte auf einer Tabelle (oder einer tabellenähnlichen Struktur) ausgeführt werden, wie in der "FROM"-Klausel angegeben. Die Anweisung kann auch eine "WHERE"-Klausel mit Einschränkungen enthalten. Für jede Zeile in der Ergebnismenge, die sich aus der Anweisung ergibt, wird dann eine Sequenznummer ausgewählt. Die folgende Anweisung führt also zu einer Sequenznummer für jede Zeile in TABLE. [sql]SELECT NEXTVALUE FOR SEQUENCE FROM TABLE[/sql] Um nur eine einzige Sequenznummer abzurufen, sollte die Anweisung genau ein Ergebnis liefern. Die Tabelle sollte also nur eine Zeile enthalten. Genau so arbeitet der HSQLDialect von Hibernate mit Sequenzen:

  • Sie erstellt eine Sequenz
  • Es erstellt eine Tabelle
  • Er fügt einen einzelnen Datensatz in die Tabelle
  • Es verwendet die Tabelle, um die nächsten Werte für die Sequenz auszuwählen

Das ist eine Menge Arbeit. Das Hauptproblem ist jedoch nicht die Arbeit, sondern die Tatsache, dass HSQLDB, das Test-RDBMS, Sequenzen auf eine völlig andere Weise behandelt als Oracle, das Produktions-RDBMS. Wie können wir nun Ibatis so konfigurieren, dass es auf einem anderen RDBMS läuft? Oh, das geht nicht. Ibatis kennt nur Anweisungen und nur eine einzige Version einer Anweisung, keine datenbankspezifischen Versionen. Umpf, eine weitere große Enttäuschung. Wir müssten also eine separate Version der SQL Map-Konfiguration zum Testen erstellen und damit den Zweck des Testens der DAO-Schicht unserer Anwendung zunichte machen. Die Tatsache, dass Hibernate über Dialekte verfügt, die ein- und ausgetauscht werden können, mag unser Urteilsvermögen trüben, aber wir glauben, dass dies die Testbarkeit einer Anwendung, die Ibatis für ihre DAO-Schicht verwendet, ernsthaft beeinträchtigt. Das Testen gegen Oracle ist sehr langsam und schwer auf allen Plattformen zu implementieren (wir müssten Oracle überall installieren). Fazit Nach einigen Stunden des Versuchs, von Hibernate zu Ibatis zu konvertieren, haben wir zwei große Enttäuschungen festgestellt: geringe Erweiterbarkeit und geringe Testbarkeit. Wir konnten nicht überprüfen, ob Ibatis die Probleme lösen würde, die wir derzeit haben. Auch die erwarteten Verbesserungen konnten wir nicht überprüfen. Also kehren wir zu unserem guten alten goldenen Hammer zurück: Hibernate! Vielleicht gibt es noch andere ORM-Frameworks, die wir untersuchen könnten. Es gibt JDO und TopLink, aber diese fortschrittlichen Lösungen werden wahrscheinlich das gleiche Problem haben wie Hibernate: ungenutzte Funktionen, die wir umgehen müssen, damit alles funktioniert. Wir könnten natürlich unser eigenes Framework mit genau den Funktionen erfinden, die wir für diese Anwendung benötigen, aber... ähm... wäre das dann noch ein Framework? Vielleicht zeigt unsere Erfahrung, dass Hibernate so gut ist , wie es nur geht Maarten Winkels Machiel Groeneveld

Verfasst von

Maarten Winkels

Contact

Let’s discuss how we can support your journey.