Hibernate Java ORM#

Hibernate ist ein ORM-Framework für [Java]]. ORM bedeutet: "Objekt-relationales Mapping" und heisst im Klartext, daß eine Datenstruktur von Objekten, so wie sie in Java üblich ist, in eine relationale Datenbank übertragen wird. Das bedeutet, daß eine Java-Applikation einfach und komfortabel auf eine echte Datenbank wie z.B. MySQL zugreifen kann. Die Daten werden somit persistent gemacht.

Zur Einführung ins Thema empfiehlt sich ein Blick auf die Webseite. Eine sehr tiefgehende Anleitung findet sich auf Java Persistenz mit Hibernate, das Thomas Bayen besitzt und wärmstens empfiehlt.

Wer jetzt neu anfängt, sollte überlegen, als Zugriffs-API direkt JPA zu verwenden. Das ist ein neuerer Java-Standard (seit Java6), der maßgeblich von Hibernate inspiriert wurde und deshalb ausgezeichnet unterstützt wird. Dennoch kann man jederzeit für besondere Anforderungen auch einzelne Hibernate-Spezialbefehle oder -Konfigurationen benutzen. Da JPA von verschiedenen ORM-Produkten zur Verfügung gestellt wird, kann man so Know-How (und ggf. sogar die ganze ORM-Bibliothek) austauschen. Es steht zu erwarten, daß die JPA-API langfristig die ursprüngliche Hibernate-API komplett verdrängen wird.

Tips und Tricks #

Bei der Arbeit mit Hibernate ist mir vor allem aufgefallen, wie komplex das Thema doch in Wirklichkeit ist. Früher dachte ich immer: Was regen die sich in all den SQL-Büchern so auf, die paar Tabellen können doch nicht so kompliziert zu bearbeiten sein. Im praktischen Betrieb mit Hibernate merkt man aber immer wieder, daß man auf Grenzen und "Bugs" stößt, die gar keine Bugs sind, sondern die in der Natur der Daten liegen, die man vorher evtl. nicht genau durchschaut hatte. Ich möchte betonen, daß ich bei aller Komplexität des Themas noch auf kein Problem gestossen bin, das man mit Hibernate nicht angemessen lösen konnte. Da mir einige dieser Aha-Momente schon wieder entfallen sind, möchte ich in Zukunft hier einen Platz haben, um sie festzuhalten.

eingebettete Arrays #

Man kann relativ einfach ein Array aus eingebetteten Komponenten erzeugen.

Wir erinnern uns: Komponenten sind Objekte, die keine eigene Identität besitzen und deshalb keine eigene Entity sind. Gedacht sind diese, damit man z.B. in Kunde eine Adresse einbinden kann, ohne daß diese Aufteilung innerhalb der Java-Klassen auch eine eigene Tabelle in der relationalen Datenbank erzeugt. Komponenten werden mit @Embeddable annotiert.

Nun kann man aber auch ein Array von Komponenten erzeugen. Dabei annotiert man ein Collection-Feld mit @CollectionOfElements. Jetzt kann man z.B. seinem Kunden mehrere Telefonnummern zuordnen. Intern geht das natürlich nicht ohne eigene Tabelle, das wird aber weitgehend vor dem Benutzer versteckt.

So weit - so gut. Für den normalen Benutzer reicht es bis hierhin. Ich bin nun aber auf ein besonderes Phänomen gestossen: Ändert man eines der Komponentenobjekte im Array, so werden als Antwort darauf beim nächsten flush() des Persistenzkontextes (also zumeist beim nächsten commit()) alle(!) Telefonnummern dieses Kunden aus der entsprechenden Tabelle mit DELETE gelöscht. Danach werden alle Telefonnummern wieder mit INSERT neu erzeugt. Bis dahin ist das erstmal umständlich und witzig, aber der Spaß hörte auf, als ich merkte, daß ein Zeiger auf eine Komponente, den ich zwischengespeichert hatte, nun nicht mehr gültig war. Das vorher gespeicherte Objekt war ja gelöscht worden, also konnte ich mit diesem nichts mehr machen - es war nicht mehr persistent.

Nach einigem überlegen fiel mir auf, das Hibernate gar nicht anders handeln kann. Meine Liste erlaubt ja grundsätzlich mehrere identische Telefonnummern. Wenn ich jetzt eine davon ändere: woran soll Hibernate erkennen, welche ich denn nun ändern will?!? Die fehlende Objektidentität zwingt also förmlich dazu, die ganze Collection zu ersetzen (zu löschen und neu zu erzeugen).

Nun war die Frage, wie man diese verlorene Identität wiederbekommen kann. Kann man, und zwar mit

  @CollectionId(columns = { @Column() }, generator = "sequence", type = @Type(type = "long"))

vor dem Collection-Feld. Diese Annotation ist laut Javadoc noch experimentell. Dementsprechend ist es zu verschmerzen, daß obige Zeile einige Informationen enthält, die ich eigentlich gar nicht einstellen will. (Für Neulinge: Hibernate wählt sonst immer sehr schön Standard-Parameter aus, so daß man vieles gar nicht konfigurieren muss.)

Diese Zeile führt dazu, daß die interne Tabelle, in der die Komponenten stehen, eine Primärschlüsselspalte bekommt. Bei der BEarbeitung der Persistenz merkt Hibernate das sofort und macht statt obigem DELETE & INSERT ein (richtigeres) UPDATE. gleichzeitig bleiben dann auch die vorhandenen Objekte persistent und deren Zeiger somit gültig.

Meine letzte Frage - die ein wenig offenbleibt - ist nun noch die, was denn nun eigentlich im Ergebnis der Unterschied zwischen dieser Lösung und der Verwendung einer echten Entity für meine Telefonnummern ist. Durch die Modellierung per Komponente hat mein Objekt offiziell (über die Java-Objekte) immer noch keine Datenbank-Identität (also keinen Primärschlüssel). Ich kann da also gar nichts falsch machen. Es ist immer sicher, daß das Löschen eines Kunden auch das Löschen einer Telefonnummer nach sich zieht. Andererseits kann man diese Beziehung per Kaskadierungs-Parametern auch für Entity-Beziehungen erzeugen. Letztlich ist es also vielleicht eine Geschmackssache.

-- ThomasBayen

Performance #

Obwohl Hibernate erstaunlich schnell sein kann, so kann man natürlich jedes Programm immer noch verbessern und beschleunigen. Ich entwickle gerade eine Datenbank-Anwendung, deren Datenbank über das Internet geschieht. Das ist so richtig schön langsam, so daß man dabei einiges über Performance lernt, was natürlich auch einem normalen Programm gut zu Gesicht steht. Ich werde versuchen, hier zu notieren, was denn nun wirklich hilft.

Zuerst mal habe ich nur einige Ideen und Stichworte gesammelt, mit denen man sich beschäftigen kann:

  • Logging des erzeugten SQL-Codes
  • Optimieren des Zugriffs
  • Connection Pooling
  • Second Level Cache

Logging des erzeugten SQL-Codes #

Über die normale Logging-Konfiguration kann man alle von Hibernate erzeugten SQL-Befehle loggen. Das sieht in meiner logback.xml so aus:

  <!-- Ausgabe des generierten SQL-Codes (...SQL) ggf. mit übergebenen Parameter-Werten (...type) -->
  <logger name="org.hibernate.SQL" level="TRACE"/>
  <logger name="org.hibernate.type" level="ERROR"/>

Mit Hilfe dieses Loggings bin ich z.B. auf das oben beschriebene Problem mit den eingebetteten Arrays gestossen.

Auf http://www.p6spy.com/ gibt es einen JDBC-Treiber, der sich zwischen die Applikation und einen "echten" JDBC-Treiber schiebt und alle Zugriffe loggt. Dessen Ausgabe ist etewas ausführlicher. Insbesondere werden alle in die SQL-Statements eingesetzten Werte sowie vor allem auch alle ResultSets geloggt. In der Praxis dürfte das normale Logging jedoch ausreichen, um Engpässe zu identifizieren.

Optimieren des Zugriffs #

Logging alleine hilft natürlich noch nichts. Dem steht die Frage gegenüber: Wie hätte ich selbst das in SQL gemacht und warum macht Hibernate das anders. Ich muss vorausschicken, daß ich es bisher immer geschafft habe, Hibernate denselben SQL-Code verwenden zu lassen, den ich auch selbst geschrieben hätte. Also das Logging betrachten und nachdenken! In dem Zusammenhang kann man übrigens auch über Lazy Loading von Unterkomponenten bzw. Unter-Collections nachdenken. Das Stichwort hier heisst fetching-Strategie.

Ein anderes Beispiel ist mir untergekommen, als das Model einer Swing-Komponente (eine JList) mit einer Hibernate-Abfrage unterlegte. Einerseits ist das eine tolle Sache, weil in der Liste immer aktuell der Inhalt der Datenbank angezeigt wird. Andererseits bemerkte ich, daß die Datenbank unendlich oft nach der Größe der Liste gefragt wurde. Sowas muss einem natürlich auffallen! Beim Debugging stellte sich heraus, daß dieser Wert beim Berechnen des Layouts immer wieder abgefragt wurde. Das zu ändern, hätte bedeutet, Swing neu zu programmieren. Als Lösung habe ich nun in meinem Model ein Flag eingebaut, um diese Größe zu cachen. Diesen Cache schalte ich in meiner Listenkomponente jeweils vor den Methoden paint() und validateTree() ein und nachher wieder aus. Und schon habe ich zwei Drittel der Datenbankabfragen eingespart!

Second Level Cache #

Ein Second Level Cache muss in Hibernate gesondert aktiviert werden. Er cachet Objekte, die im normalen Pesistenzkontext (First Level Cache) gespeichert werden, nochmal. Dieser Cache bleibt dann auch bestehen, wenn ein neuer Kontext aufgemacht wird. Grundsätzlich ist dazu zu sagen, daß zu viel cachen nur was bringt, wenn man kaum Schreiboperationen auf seinen Daten hat, da man ansonsten ständig in Sorge um die Konsistenz seiner Daten lebt. Derartige Probleme kann man in Hibernate zwar auch sehr schön lösen, aber das geht halt auch immer nur mit einem Zugriff auf die Datenbank, was dann den Cache nutzlos macht.

Dazu kommt das Problem, das nur bestimmte Sorten von Abfragen überhaupt gecachet werden können. Alles in allem macht das nur Sinn, wenn man wirklich viele Zugriffe, die fast nur lesend sind, auf eine Datenbank hat. Das kann z.B. in einer Webapplikation passieren.

Für meinen Fall war das keine Lösung, da ich unbedingte Konsistenz (und damit Aktualität) meiner Daten benötige. Wer sich für das Thema interessiert, dem empfehle ich allerdings mein Hibernate-Buch. :-) -- ThomasBayen

Links zum Thema Hibernate-Performance #

Enumerations in Hibernate #

In aktuellen Versionen von Hibernate kann man problemlos auch Enum-Werte speichern. Dabei kann man per Annotation angeben, ob diese als Integer (über ihren Ordinalwert) oder als String in die Datenbank geschrieben werden.

Enums in PostgreSQL #

Eigentlich ist das sehr komfortabel und lässt kaum Wünsche offen, aber ich habe erkannt, daß es doch noch besser geht. In der PostgreSQL Datenbank gibt es einen besonderen Datentyp für enum-Werte. Das hat den Effekt, daß intern mit einem Integer gearbeitet wird, was schnelle Operationen ermöglicht, aber das in Abfragen der besser lesbare Stringwert benutzt werden kann.

Leider werden Datenbank-Enums nicht direkt von Hibernate unterstützt. Man kann jedoch wie für jeden selbsterfundenen Datentyp einen UserType dafür definieren. Das ist ganz gut hier beschrieben: http://anismiles.wordpress.com/2010/08/04/postgres-enum-with-hibernate/ . Das bedeutet dann allerdings, daß man an jede Enum-Property zwei Annotationen dranschreiben muss, um ihren Typ ordentlich zu deklarieren.

Bei näherer Betrachtung der Syntax ist mir der Gedanke gekommen, daß, wenn eine Tabelle in PostgreSQL einmal definiert ist, eigentlich kein Unterschied mehr zwischen String-Enums und Enum-Enums zu sehen ist (solange man keine falschen Werte angibt, was Enum-Enums nicht erlauben). Es könnte also sein, daß man die Tabelle nur anders deklarieren muss und ansonsten Hibernate auf Strings einstellt. Es könnte allerdings auch sein, daß der JDBC-Treiber es nicht mag, wenn man ein String-Objekt in ein Feld schiebt, welches vom Typ ~PGObject ist. Das bleibt also noch auszuprobieren.


Tags:  Java, Datenbank

Add new attachment

Only authorized users are allowed to upload new attachments.
« This page (revision-4) was last changed on 04-Jan-2011 17:09 by ThomasBayen