Home ORDIX AG             Dienstleistung             Trainingsshop    Kunden / Referenzen Aktuelles    Kontakt
Home  Pfeil  ORDIX News  Pfeil  2/2007  Pfeil  Java/J2EE/JEE
suche: 
Dieser Artikel richtet sich an Java-Entwickler, die bereits mit dem Umgang von Hibernate vertraut sind.

Glossar

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 Ins­tallationen 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



Lang laufende Transaktionen und Optimistic Locking – Reihe Hibernate (Teil IV):

Lange Gespräche mit Hibernate


Die meisten Anwendungsfälle funktionieren immer nach dem Schema: laden, anzeigen, ändern, speichern. Die Arbeitsschritte sind klar abgegrenzt und der Entwickler hat leichtes Spiel. Was ist aber, wenn es in einer Web-Anwendung gilt, sich über mehrere Arbeitsschritte hinweg Änderungen zu merken, ohne diese zu speichern, z. B. in einem mehrschrittigen Registrierungsprozess? Plötzlich könnte alles schrecklich kompliziert sein – mit Hibernate jedoch nicht.

Die Session, ganz alltäglich

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.

Session per Request Modell
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?

Ein Beispiel: Wizard

Für die folgenden Betrachtungen soll das Bei­spiel 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 Änderun­gen 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.

Hibernate hilft mit Long Conversation

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.

Wie funktioniert die Umsetzung?

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.

Session per Request with Detached Objects Modell
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.begin­Transaction() 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.

Das Session per Conversation Modell
Abb. 3: Das Session per Conversation Modell.

Parallele Prozesse: Wir sind nicht allein

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 An­wender 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.

Automatische Versionierung

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 an­schließ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.

Fazit

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 Flush­Mode.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).