
|
| 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. 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. Vergrößern |
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).
Um in Hibernate-Objekte in den Cache zu laden, existieren die folgenden zwei Zugriffsarten.
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).
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).
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.
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.
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.
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.
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).