Die Kombination von Hibernate und komplexen Datenstrukturen bringt einige Herausforderungen mitsich und daher ist dem ein oder anderen die Exception aus dem Titel sicher geläufig. In einigen Fällen hat man an solchen Punkten eine fast fertige Anwendung und es geht an die Implementierung von Jobs oder neuen Anforderungen. Nun lassen sich diese meist nicht, oder sehr schwer, mit den vorhandenen Methoden abbilden und man fängt an, durch die Hintertür einige Abfragen zusammenzubauen um an die entsprechenden Daten zu kommen. Gerade auch bei Jobs werden mitunter Daten geladen, welche querfeldein durch die bestehenden Methoden der Anwendung geschickt werden. Leider bleibt hier desöfteren ein wichtiger Teil auf der Strecke - die Verbindung zur Hibernate Session.
Lazy-Loading mit Proxy-Objekten
Grundsätzlich sind alle Ladevorgänge aus der Datenbank "Lazy-Loading". Dies soll verhindern, dass man die halbe Datenbank in den Speicher lädt falls die Beziehungen zwischen den Tabellen eng verwoben sind. Praktisch bedeutet das, Hibernate lädt bei Abfrage von Objekten zugehörige Referenzen (Collections/ Unterobjekte) als Proxies.
Ein Proxy-Objekt enthält dabei nur die Id des Objektes in der Datenbank und beim ersten Zugriff wird es automatisch nachgeladen. Der Nachteil an der Sache ist aber nun, wenn die Hibernate-Session geschlossen wird, befindet sich das gerade geladenen Objekt im Zustand detached
.
Das bedeutet, ein Objekt ist nicht mehr in Synchronisation mit der Datenbank und demzufolge können auch für die Proxy-Objekte keine Daten mehr nachgeladen werden - die Anwendung wirft eine LazyInitializationException
.
Manuelles Attach
Nun gibt es mehrere Möglichkeiten etwas dagegen zu tun, schauen wir uns zunächst mal die Einfachste an. In der Methode in der auf Parameter eines Objektes zugegriffen wird, prüft man einfach ob die Verbindung zur Session noch da ist und falls nicht, dann wird das Objekt eben wieder drangehangen (attached
).
def someAction(Author author) {
....
if(!author.isAttached()) {
author.attach()
}
}
FetchMode via Domain-Definition
Oft hat man jedoch nicht die Möglichkeit in den Methoden das Objekt wieder an die Session zu hängen. Entweder ist die benutzte API bereits fertig oder man muss sehr viele Methoden anpassen.
Nun gibt es dafür aber eine ebenfalls einfache Lösung: In Grails kann man den FetchMode, also die Strategie zum Nachladen der referenzierten Objekte, direkt in der jeweiligen Domain-Klasse definieren. Der folgende Code demonstriert das anschaulich:
class Author {
String firstname
String surname
static hasMany = [ books : Book ]
static fetchMode = [ books : 'eager' ]
}
class Book {
String name
static belongsTo = Author
static hasMany = [categories : Category]
static fetchMode = [categories : 'eager']
}
class Category {
String name
static belongsTo = Book
}
Der Vorteil dieser Lösung ist, dass man sich wahrscheinlich an keiner Stelle der Anwendung mehr Gedanken um das Nachladen der Bücher des Authors samt zugehöriger Kategorien machen muss. Hat man viele Bücher bzw. Kategorien zieht man sich jeoch, wie bereits oben erwähnt, ziemlich viele Daten in den Speicher - auch in Situationen in denen man beispielsweise nur alle Authoren auflisten möchte.
FetchMode via Queries
Wesentlich eleganter als die obere Strategie, ist es die benötigten Objekte beim Laden der Daten mit anzuzeigen. Über den Hibernate CriteriaBuilder lässt sich der FetchMode bequem für jedes Property einzeln definieren. "Nested Properties", also verschachtelte Objekte werden durch den Punkt-Operator in der Query referenziert. Die nachfolgende Abfrage holt alle Authoren, samt Bücher und Kategorien:
import org.hibernate.FetchMode as FM
....
def criteria = Author.createCriteria()
def result = criteria.list {
fetchMode("books", FM.EAGER)
fetchMode("books.categories", FM.EAGER)
}
In einigen Fällen hat man hier das Problem, dass die Datensätze mehrfach in der Liste erscheinen. Hibernate führt beim "Eager-Fetch" einen Outer Join durch und deshalb duplizieren sich die Einträge für gefundene Unterobjekte . Eine Lösung dafür ist criteria.listDistinct{}
anstatt criteria.list{}
zu verwenden.
Ein Nachteil dieser Lösung soll auch nicht verschwiegen werden: Zwar bekommt man die Referenzen in den Speicher geladen welche man möchte, nur muss man vorher genau wissen was man an anderer Stelle benötigt. Wird eine Methode mit dieser Query aufgerufen und es werden gar nicht alle Bücher/Kategorien gebraucht, verschenkt man auch hier wieder Performance.
Anmerkung: Dasselbe kann man natürlich auch mit HQL bzw. DynamicFinder machen, hier übergibt man den FetchMode dann als Parameter der Funktion.