Home ORDIX AG             Dienstleistung             Trainingsshop    Kunden / Referenzen Aktuelles    Kontakt
Home  Pfeil  ORDIX News  Pfeil  3/2009  Pfeil  Java/JEE
suche: 
Dieser Artikel richtet sich an Softwareentwickler- und architekten, die Hibernate performant einsetzen möchten.


Performance-Tests mit Hibernate (Teil III)

Lazy und Eager Loading - ein guter Cache lädt nicht mehr als er muss


Müssen wir beim Laden von Objekten in den Cache wirklich jedes mal alle Objekte laden? Gibt es nicht eine Möglichkeit, nur die Objekte zu laden, die auch gebraucht werden? Diesen Fragen widmen wir uns in diesem Artikel und untersuchen, welche Möglichkeiten Hibernate bietet und welche Auswirkungen fehlerhafte Einstellungen auf die Performance haben können.

Abb. 1: Klassenmodell des Beispielprojekts.
Abb. 1: Klassenmodell des Beispielprojekts. Vergrößern
 
1	Vorlesung vorlesung = (Vorlesung) session.load(Vorlesung.class , id) ;
2	vorlesung.getId() ; 
3	vorlesung.getName();
Abb. 2: Verwendung eines Proxies.
 
1 @Column(name="Name")
2 public String name 
Abb. 3: Direkter Feldzugriff.
 
Abb. 4: Vorlesungsobjekt.
Abb. 4: Vorlesungsobjekt. Vergrößern
 
1 @Entity
2 @Table(name = "VORLESUNG")
3 @Proxy(lazy=false)
4 public class Vorlesung {...}
Abb. 5: Proxy deaktivieren.
 
1 @ManyToOne(fetch = FetchType.LAZY)
2 public Dozent getDozenten() {return dozenten;}

1 @ManyToMany(fetch = FetchType.EAGER)
2 public Collection<Student> getStudenten() {return studenten;}
Abb. 6: Fetching-Strategien ändern.
 
Abb. 7: Messergebnisse des Lazy und Eager Loading.
Abb. 7: Messergebnisse des Lazy und Eager Loading. Vergrößern
 

Einleitung

Nachdem wir uns zu Beginn der Artikelreihe mit dem First Level, Second Level und Query Cache von Hibernate auseinandergesetzt haben [1], wurde im 2. Teil der Reihe der Easy Hibernate Cache näher unter die Lupe genommen [2]. Als Grundlage für diesen Artikel dient erneut das altbekannte Klassenmodell aus dem 1. Artikel (siehe Abbildung 1).

Mal träge, mal eifrig ...

Um in Hibernate-Objekte in den Cache zu laden, existieren die folgenden zwei Zugriffsarten.

Lazy Loading - der "träge" Nachzügler

Beim Auslesen von persistenten Objekten benutzt Hibernate als Default für alle Entities und Collections eine Lazy Fetching Strategie. Dabei werden bei Verwendung eines Proxies nur die Objekte in den Cache geladen, nach denen eine Abfrage gestartet wurde. Ein Proxy ist ein Platzhalter, der zur Laufzeit generiert wird. Ein sinnvolles Beispiel für Lazy Loading ist eine Auswertung aller Vorlesungen, ohne dass dabei die Dozenten und Studenten geladen werden müssen.

In Abbildung 2 wird dies anhand eines Beispiels veranschaulicht. Zunächst wird mit Hilfe der Methode load() über einen Identifikator eine Vorlesung geladen. In Hibernate steht nach diesem Aufruf allerdings kein Vorlesungsobjekt im Cache. Stattdessen wird ein Proxy erstellt, das als "leeres" Objekt verstanden werden kann. Der Proxy besitzt jedoch außer einem Identifikator noch keine Daten. Wenn ein Zugriff über die Getter-Methoden auf die Objekteigenschaften stattfindet, wird ein SQL-Statement generiert und die Objekteigenschaften werden nachgeladen.

Der Aufruf der Methode getId() löst hingegen keinen Datenbankzugriff aus, weil für Identifikatoren keine Initialisierung des Proxies nötig ist. Ein Datenbankzugriff findet erst statt, wenn die Methode getName() in Zeile 3 ausgeführt wird und somit auf die Objekteigenschaften zugegriffen wird. Weiterhin ist zu beachten, dass immer ein Datenbankzugriff stattfindet, wenn Annotations auf direkten Feldzugriff gemappt werden (siehe Abbildung 3).

Eager Loading - der "eifrige" Vorarbeiter

Proxies bieten sich bei Objekten an, dessen Objekteigenschaften nicht immer gebraucht werden. Aber es gibt auch Situationen, in denen Daten direkt komplett in den Cache geladen werden sollen, ohne dass weitere Datenbankzugriffe stattfinden (Eager Loading).

Ein Beispiel ist, dass beim Laden eines Vorlesungsobjekts auch direkt das Objekt des Dozenten und des Lehrstuhls mitgeladen werden soll. Abbildung 4 zeigt das Vorlesungsobjekt, das ohne Generierung eines Proxies gespeichert worden ist. Mit der Methode get() wurde eine Möglichkeit beschrieben, die ein Nachladen der Objekteigenschaften auslöst. Mit der Annotation @Proxy(lazy=false) ist es möglich, die Proxy-Generierung für eine bestimmte Klasse zu deaktivieren (siehe Abbildung 5).

Assoziationen Eager und Lazy fetchen

In dem Klassendiagramm existiert eine Viele-zu-Eins-Beziehung zwischen den Objekten Vorlesung und Dozent, sowie eine Eins-zu-Eins-Beziehung zwischen den Objekten Dozent und Lehrstuhl. Assoziationen können ebenfalls Lazy oder Eager geladen werden. Das hängt davon ab, ob mit einem Objekt auch die in Beziehung stehenden Objekte sofort in den Cache geladen werden oder nicht.

In Hibernate werden Beziehungen standardmäßig Lazy geladen. Durch Deaktivierung der Proxy-Generierung für das Objekt Vorlesung, werden die Objekte Dozent und Lehrstuhl Eager geladen (siehe Abbildung 4). Es ist möglich, die Fetching-Strategien für Assoziationen zu verändern, indem in der Parameterliste der Annotation die Fetching-Strategie festgelegt wird (siehe Abbildung 6). In diesem Beispiel wird die Viele-zu-Eins-Beziehung Lazy und die Viele-zu-Viele-Beziehung Eager geladen.

Eager und Lazy Loading mit Hibernate Query Language (HQL)

Mit HQL ist ebenfalls die Unterscheidung zwischen Lazy und Eager Loading möglich. Die Methoden list() und find() verwenden die Eager Fetching Strategie, während die Methode iterate() dem Lazy Fetching-Ansatz entspricht.

Die Methode list() verfolgt folgenden Ansatz: Nachdem die HQL-Anfrage auf der Datenbank ausgeführt wurde, werden die Ergebnismengen in eine Java-Liste gespeichert. Dabei wird das Objekt vollständig in den Cache geladen. Im Cache werden die erstellten Objekte mit dem Cache-Inhalt geprüft. Existiert das Objekt bereits im Cache, wird es nicht aktualisiert. Wenn das Objekt nicht im Cache vorhanden ist, wird es dort gespeichert. Die find()-Methode verfolgt den gleichen Ansatz, liefert das Ergebnis jedoch in einem unsortierten Java Set zurück. Die Methode iterate() gibt die gleichen Daten wie list() zurück, ermittelt aber nur die Objektidentifikatoren der Trefferobjekte. Wenn bereits ein Objekt im Cache vorhanden ist, wird es aus dem Cache geladen. Andernfalls wird ein Proxy generiert.

Um die Auswirkungen der Fetching-Strategien zu untersuchen, wird im Folgenden ein Szenario beschrieben und die Ergebnisse ausgewertet.

Szenariobeschreibung

Es werden zwei Durchläufe gestartet. Beim ersten Durchlauf wird mit HQL die Methode list() verwendet (Eager Loading), beim zweiten Durchlauf die Methode iterate() (Lazy Loading). Nach 50 Abfragen findet ein Zugriff auf die Objekteigenschaften von Vorlesung, Dozent und Lehrstuhl statt.

Trägheit kann bestraft werden

Wie in Abbildung 7 zu erkennen ist, benötigt die Strategie Lazy Loading mit durchschnittlich 3 ms für die ersten 50 Anfragen gegenüber Eager Loading mit 46 ms weniger Zeit für eine Abfrage. Für die restlichen Abfragen benötigt Lazy Loading durchschnittlich 1218 ms und Eager Loading wieder 46 ms.

Beim Eager Fetching werden alle Vorlesungen und alle zugehörigen Objekte zusammen mit einem einzigen Select-Statement in den Cache abgelegt. Die Lazy Loading-Strategie selektiert nur den Identifikator der Vorlesung und generiert Proxies für die Beziehungen. Dadurch wird die Datenmenge reduziert, die aus der Datenbank gelesen wird. Aus diesem Grund ist Lazy Loading bei den ersten 50 Durchläufen schneller als Eager Loading.

Nach 50 Abfragen wird auf die Objekteigenschaften zugegriffen. Beim Eager Fetching sind die Daten bereits vorhanden. Beim Lazy Loading müssen hingegen nachträglich alle Objekteigenschaften nachgeladen werden. Dabei selektiert ein Select-Statement die Objekteigenschaften der Vorlesung mit dem Identifikator. Danach wird ein weiteres Select-Statement erzeugt, das die Objekteigenschaften des Dozenten und des Lehrstuhls lädt. Dieser Vorgang wird für alle gefundenen Vorlesungen wiederholt (n+1 selects-Problem) und ist somit wesentlich ineffizienter als die Strategie Eager Loading.

Fazit

Bevor man sich für eine der beiden Strategien entscheidet, sollte man sich vorher Gedanken darüber machen, ob es sinnvoll ist, alle Objekteigenschaften in den Cache zu laden oder ob ein Platzhalter ausreichend ist.

Werden die Objekteigenschaften häufig gebraucht, ist es sinnvoll die Eager-Strategie anzuwenden, weil dadurch ein Zugriff auf die Datenbank vermieden wird und man sich das Nachladen der Objekteigenschaften spart. Falls ein Nachladen der Objekte vermieden werden soll, ist es ratsamer die Lazy-Strategie zu verwenden.

Vorsicht ist jedoch besonders bei der Strategie Lazy Loading geboten. Bei fehlerhaftem Einsatz kann es zu Performance-Einbußen kommen, wie anhand des Performance-Tests zu erkennen war. Trägheit kann also bestraft werden. Aber es gibt auch Situationen, in denen sie förderlich ist ☺.

Alexander Keil (info@ordix.de).