
| User Think Time Die Zeitspanne, in der die Anwendung auf Benutzereingaben wartet. |
| Long Conversation Bezeichnet einen Anwendungsfall, der mehrere Zyklen von User Think Time enthält. |
| Wizard Eine Abfolge von Dialogen, die in mehreren Schritten Daten vom Benutzer abfragt. Wird häufig zur Vereinfachung von Installationen oder Konfigurationen eingesetzt. |
| Flush Schreiben der veränderten Daten aus den POJOs in die Datenbank. |
| FlushMode Steuert den Zeitpunkt des Flush. Standard ist das Ende der Transaktion. |
| Detached Objects Persistente Objekte, deren Session geschlossen wurde, wechseln in diesen Zustand. Sie können zu einem späteren Zeitpunkt mit einer neuen Session verknüpft werden und somit wieder in den Zustand persistent wechseln. |
Weiterführende Links
Die Session ist in einer Anwendung auf Basis von Hibernate der Schlüssel zur Interaktion mit der Datenbank. Es gibt sehr viele Möglichkeiten zur Konfiguration der Session und noch viele Möglichkeiten mehr bei der Anwendung. Im Verlauf des Artikels werden wir uns das Verhalten der Session bezogen auf den Zeitpunkt des Speicherns von veränderten Daten (Flush) sowie ihre Lebensdauer näher anschauen.
Das Standardverhalten, das allen Hibernate-Entwicklern vertraut sein sollte, ist, dass sich die Session immer parallel zu einer Datenbanktransaktion "bewegt". Direkt nach dem Beziehen der Session wird eine Transaktion über diese gestartet. Danach lädt und verändert die Applikationslogik im Rahmen dieser Transaktion ein persistentes Objekt. Am Ende wird die Transaktion committed und die Session schreibt dabei die Änderungen an von ihr geladenen Objekten in die Datenbank (flush). Anschließend beendet sich die Session selbst (close), wobei auch alle von der Session geladenen Objekte in den entkoppelten (detached) Zustand wechseln.
![]() |
| Abb. 1: Das Session per Request Modell. |
Für viele Anwendungsfälle ist dieses als Session per Request bezeichnete Verhalten ideal. Der Entwickler muss sich nicht um das Speichern seiner Veränderungen kümmern. Er muss die Objekte nur durch die Session laden und verändern, alles andere erledigt Hibernate von selbst. Aber was ist, wenn einer der letzten beiden Schritte – das Schreiben und Entkoppeln – nicht sofort erfolgen soll?
Für die folgenden Betrachtungen soll das Beispiel eines mehrstufigen Benutzerdialogs (Wizard) dienen. Mehrere Masken folgen aufeinander und der Benutzer kann beliebig zwischen den Masken vor und zurück navigieren. Erst am Ende dieser Abfolge, also nach dem Ausfüllen der letzten Maske, sollen die veränderten Daten gespeichert werden. In der Anwendung hätte man zunächst einen initialen Aufruf, der die Daten für die erste Maske lädt und zur Anzeige bringt. Danach folgt jeweils zwischen der Darstellung von zwei Masken das Entgegennehmen der geänderten Daten aus der vorherigen Maske und dann das Laden der Daten für die folgende Maske. Am Ende des Gesamtprozesses würden die Daten der letzten Maske entgegengenommen werden und dann der speichernde Aufruf erfolgen.
Ein Problem dabei ist, dass jede Datenbankinteraktion durch die Session innerhalb einer Transaktion stattfinden muss. Hibernate verpflichtet den Entwickler dazu. Wie oben erwähnt, ist das Standardverhalten der Hibernate Session aber so, dass Änderungen am Ende einer Transaktion durch einen impliziten flush() gespeichert werden. Session per Request würde hier folglich bewirken, dass Änderungen sofort beim Wechsel von einer Maske zur nächsten gespeichert werden.
Ein erster, einfacher Ansatz, um dieses Problem zu umgehen, würde vorsehen, die Transaktion im initialen Aufruf zu starten und erst am Ende des Gesamtprozesses zu speichern. Dies hat aber den gravierenden Nachteil, dass die Transaktion auch die User-Think-Time, also die Zeit, die ein User benötigt, um die nächste Maske auszufüllen, umspannt. Während dies für einen oder wenige Anwender sicherlich noch kein (großes) Problem ist, wird dies mit wachsender Anzahl von Benutzern zum K.O.-Kriterium. Über kurz oder lang werden die vielen offenen Transaktionen die Datenbank ausbremsen – die Anwendung skaliert nicht.
Die Lösung des Problems ist einfach und besteht in einer so genannten Long Conversation. Darunter versteht Hibernate die Entkoppelung von Session und Transaktion, indem kein impliziter Flush mehr erfolgt. Stattdessen muss der Entwickler am Ende eines Gesamtprozesses dafür sorgen, dass seine Anwendung den Flush explizit durch Aufruf von Session.flush() ausführt. Dies kann konfiguriert werden, indem der FlushMode der Session zu Beginn des Gesamtprozesses mit Session.setFlushMode (FlushMode.NEVER) umgestellt wird. Der Ablauf der Anwendung wäre nun wie oben geschildert. In allen Masken können Änderungen vorgenommen werden, ohne dass der Stand innerhalb der Datenbank verändert würde. Erst während der abschließenden Transaktion würde der Flush der Änderungen ausgelöst.
Bei der Umsetzung einer Long Conversation bietet Hibernate die Möglichkeit, die Lebensdauer der Session zu steuern. Zur Auswahl stehen zwei Varianten (Patterns):
Der Unterschied zwischen den Varianten besteht darin, ob nach der Transaktion eines jeden Maskenaufrufs auch die Session geschlossen und die persistenten Objekte somit entkoppelt werden oder nicht. Bei Detached Objects ist dies der Fall (siehe Abbildung 2). Beim Start der Anwendungslogik der nächsten Maske wird eine neue Session generiert und mit Hilfe von Session.update(Object) würde ein verändertes Objekt an die neue Session gebunden.
![]() |
| Abb. 2: Das Session per Request with Detached Objects Modell. |
Bei der Alternative Extended Session (siehe Abbildung 3) wird auf das Schließen der Session verzichtet. Beim Start der Anwendungslogik der nächsten Maske wird die aus dem vorherigen Request vorhandene Session weiterverwendet. Dazu muss lediglich eine neue Transaktion über Session.beginTransaction() gestartet werden. Da die persistenten Objekte nicht von ihrer Session entkoppelt werden, ist es bei dieser Vorgehensweise auch nicht notwendig, ein Session.update() durchzuführen.
![]() |
| Abb. 3: Das Session per Conversation Modell. |
Die Sperren in der Datenbank wurden durch die geschilderten Maßnahmen auf ein Minimum reduziert. Somit wurde auf technischer Basis dafür gesorgt, dass die Anwendung gut skaliert und auch viele Anwender gleichzeitig tragbar sind. Implizit hat sich nun aber ein neues Problem in Form von parallel laufenden Prozessen ergeben. Technisch ist es nun möglich, dass zwei (oder mehr) Benutzer den gleichen Anwendungsfall und die gleichen Daten parallel nutzen und dabei unterschiedliche Änderungen vornehmen. Es kommt zum Konflikt. Ohne eine Steuerung dieser Nebenläufigkeit werden immer die Änderungen, die zuletzt gespeichert werden, wirksam (last commit wins).
Ein in solchen Situationen häufig genutzter Mechanismus ist die "Optimistic Concurrency Control". Als optimistisch wird dieser Ansatz deshalb bezeichnet, weil er nicht auf Sperren setzt, die es ja gerade zu vermeiden gilt. Statt dessen geht man davon aus, dass alle anderen Zugriffe keine Konflikte erzeugen. Vor dem bzw. beim Schreiben in die Datenbank findet eine Kontrolle statt, ob Änderungen, die in Konflikt zu den eigenen stehen, vorgenommen wurden. Wenn nein, werden die Daten übernommen. Wenn ja, ist es an der Anwendung, eine Lösung zu finden. Ein typisches Vorgehen würde den Anwender über die parallel durchgeführten Änderungen informieren und z. B. einen manuellen Abgleich ermöglichen.
Die Kontrolle, ob parallele Änderungen stattgefunden haben, kann beliebig implementiert werden. Als effektiv hat sich die Versionierung von Datensätzen mit Versionsnummern oder Zeitstempeln erwiesen. Dafür muss das Datenmodell um eine Spalte für die Version erweitert werden. Ist eine Anpassung des Datenmodells nicht möglich, kann auch ein Vergleich aller (veränderten) Werte zwischen dem persistenten Objekt und der Datenbank erfolgen. Beide Ansätze werden von Hibernate unterstützt, so dass der notwendige Abgleich nicht mehr vom Entwickler der Anwendung implementiert werden muss.
Die Versionierung muss lediglich im Mapping der persistenten Klasse konfiguriert werden. Dazu wird die entsprechende Tabellenspalte mit Hilfe des Elements <version> angegeben. Beim Laden eines persistenten Objekts durch die Session wird die Version ebenfalls geladen. Beim flush() werden vor dem Schreibzugriff in der Datenbank die Versionen von Session und Datenbank für das zu speichernde Objekt verglichen. Sind die Versionen identisch, wird die Version von Hibernate automatisch aktualisiert und die Änderungen anschließend persistiert. In einem parallel laufenden Prozess würde die Prüfung der Versionen anschließend fehlschlagen und die Anwendung muss entsprechend reagieren können.
Ist eine Anpassung des Datenbankschemas nicht möglich, z. B. weil eine ältere Anwendung das Schema nutzt, kann Hibernate auch die Werte einer Instanz einzeln mit dem Stand der Datenbank vergleichen. Dazu muss im <class>-Element des Mappings das Attribut optimistic-lock auf all gesetzt werden. Wenn parallele Änderungen an einem Objekt nicht schädlich sind, so lange sich die Änderungen nicht überlagern, kann auch dirty als Wert gesetzt werden. In diesem Fall sind beim flush() nicht alle, sondern nur die veränderten Felder für den Abgleich mit der Datenbank relevant.
Hibernate unterstützt den Entwickler bei der Entwicklung von skalierbaren Anwendungen enorm. Mit der Entkopplung von Session und Transaktion wird die Implementierung atomarer Operationen auf das Setzen eines FlushMode.NEVER und explizites Aufrufen von Session.flush() reduziert. Dass dabei möglicherweise Konflikte durch parallele Änderungen entstehen, kann Hibernate nicht verhindern. Aber mit den Automatismen für die Versionierung bzw. Überprüfung auf gleichzeitige Veränderungen bietet es zwei hilfreiche Werkzeuge, um der Lage Herr zu werden. Mehr Details hierzu erfahren Sie auch in unserem Seminar "Entwicklung mit Hibernate". Nähere Informationen und Anmeldungsmöglichkeiten dazu finden Sie im Trainingsshop.
Michael Heß (info@ordix.de).