In den drei vorangegangenen Blogs über JPA-Implementierungsmuster habe ich die Basisoperationen zum Speichern von Entitäten, zum Abrufen von Entitäten und zum Entfernen von Entitäten behandelt. In diesem Blog werde ich einen anderen Blickwinkel einnehmen und untersuchen, wie Entitäten nachgeladen werden und wie sich dies auf Ihre Anwendung auswirkt. Jeder, der schon eine Weile mit Hibernate arbeitet, hat wahrscheinlich schon einmal eine LazyInitializationException oder zwei gesehen, meist gefolgt von einer Meldung wie "failed to lazily initialize a collection of role: com.xebia.jpaip.order.Order.orderLines, no session or session was closed" oder "could not initialize proxy - no Session". Auch wenn diese Meldungen neue Benutzer von Hibernate verwirren mögen, so sind sie doch viel besser als die NullPointerExceptions, die OpenJPA in diesen Fällen ausgibt (zumindest wenn Sie die Laufzeit-Bytecode-Erweiterung verwenden). Um JPA in vollem Umfang nutzen zu können, müssen Sie unbedingt verstehen, wie Lazy Loading funktioniert, denn es ermöglicht Ihnen, Ihre komplette Datenbank mit all ihren Beziehungen zu modellieren , ohne die gesamte Datenbank zu laden, sobald Sie nur auf eine Entität zugreifen.
Daher ist es bedauerlich, dass die JPA 1.0-Spezifikation dieses Thema nicht ausführlicher behandelt als mit ein paar Sätzen wie:
Die EAGER-Strategie ist eine Anforderung an die Laufzeit des Persistenzproviders, dass die Daten eifrig geholt werden müssen. Die LAZY-Strategie ist ein Hinweis an die Laufzeit des Persistenzproviders, dass die Daten beim ersten Zugriff "lazily" geholt werden sollen. Die Implementierung darf Daten, für die der LAZY-Strategie-Hinweis angegeben wurde, eifrig abrufen.
Zum jetzigen Zeitpunkt enthält der vorgeschlagene endgültige Entwurf der JPA 2.0-Spezifikation in dieser Hinsicht keine Neuerungen. Das Beste, was wir im Moment tun können, ist, die Dokumentation unseres JPA-Anbieters zu lesen und einige Experimente durchzuführen.
Wann das faule Laden auftreten kann
Alle @Basic, @OneToMany, @ManyToOne, @OneToOne, und @ManyToMany-Annotationen haben einen optionalen Parameter namens fetch. Wenn dieser Parameter auf FetchType.LAZY wird als Hinweis für den JPA-Anbieter interpretiert, dass das Laden dieses Feldes verzögert werden kann, bis es zum ersten Mal aufgerufen wird:
- den Eigenschaftswert im Falle einer @Basic-Anmerkung,
- die Referenz im Falle einer @ManyToOne- oder @OneToOne-Anmerkung, oder
- die Sammlung im Falle einer OneToMany- oder einer @ManyToMany-Anmerkung.
Standardmäßig werden Eigenschaftswerte eifrig und Sammlungen träge geladen. Im Gegensatz zu dem, was Sie vielleicht erwarten, wenn Sie schon einmal mit Hibernate gearbeitet haben, werden Referenzen standardmäßig eifrig geladen. Bevor wir uns mit der Verwendung von Lazy Loading beschäftigen, lassen Sie uns einen Blick darauf werfen, wie Lazy Loading von Ihrem JPA-Anbieter implementiert werden kann.
Bytecode-Instrumentierung zur Erstellungszeit, Bytecode-Instrumentierung zur Laufzeit und Laufzeit-Proxys
Damit das Lazy Loading funktioniert, muss der JPA-Anbieter etwas zaubern, damit die Objekte, die noch nicht vorhanden sind, so aussehen, als ob sie vorhanden wären. Es gibt eine Reihe verschiedener Möglichkeiten, wie JPA-Anbieter dies erreichen können. Die gängigsten Methoden sind:
- Bytecode-Instrumentierung zur Erstellungszeit - Die Entitätsklassen werden direkt nach der Kompilierung und vor der Paketierung zur Ausführung instrumentiert. Der Vorteil dieses Ansatzes ist, dass die Bytecode-Instrumentierung die beste Leistung und die am wenigsten undichte Abstraktion bieten kann. Ein Nachteil ist, dass Sie Ihre Build-Prozedur ändern müssen und (daher) nicht immer mit IDEs kompatibel sind. Die instrumentierten Klassen können binär inkompatibel mit ihren uninstrumentierten Versionen sein, was zu Java-Serialisierungsproblemen und ähnlichem führen kann, aber das ist etwas, was ich noch nicht als Problem gehört habe.
- Bytecode-Instrumentierung zur Laufzeit - Anstatt die Entitätsklassen zur Build-Zeit zu instrumentieren, können sie auch zur Laufzeit instrumentiert werden. Dies erfordert die Installation eines Java-Agenten mit der Option -javaagent ab JDK 1.5, die Rücktransformation von Klassen, wenn Sie unter JDK 1.6 oder einer späteren Version arbeiten, oder eine proprietäre Methode, wenn Sie ein älteres JDK verwenden. Bei dieser Methode müssen Sie zwar Ihre Build-Prozedur nicht ändern, aber sie ist sehr spezifisch für das von Ihnen verwendete JDK.
- Laufzeit-Proxies - In diesem Fall werden die Klassen nicht instrumentiert, sondern die vom JPA-Anbieter zurückgegebenen Objekte sind Proxies für die tatsächlichen Entitäten. Bei diesen Proxies kann es sich um dynamische Proxy-Klassen, Proxies, die von CGLIB erstellt wurden, oder Proxy-Sammlungsklassen handeln. Diese Methode erfordert zwar die wenigsten Einstellungen, ist aber auch die am wenigsten transparente Methode, die den JPA-Implementierern zur Verfügung steht, und erfordert daher die meisten Kenntnisse.
Proxy-basiertes träges Laden zur Laufzeit mit Hibernate
Hibernate unterstützt zwar die Bytecode-Instrumentierung zur Build-Zeit, um das faule Laden einzelner Eigenschaften zu ermöglichen, aber die meisten Benutzer von Hibernate werden Laufzeit-Proxies verwenden; dies ist der Standard und funktioniert in den meisten Fällen gut. Schauen wir uns also die Laufzeit-Proxys von Hibernate an. Hibernate erstellt zwei Arten von Proxys:
- Wenn Sie eine Entität über eine faule Viele-zu-Eins- oder Eins-zu-Eins-Verknüpfung oder durch den Aufruf von EntityManager.getReferenceaufruft, verwendet Hibernate CGLIB, um eine Unterklasse der Entitätsklasse zu erstellen, die als Proxy für die echte Entität fungiert. Beim ersten Aufruf einer Methode dieses Proxys wird die Entität aus der Datenbank geladen und der Methodenaufruf wird an die geladene Entität weitergegeben. Mein Kollege Maarten Winkels hat letztes Jahr über die Fallstricke dieser Hibernate-Proxys gebloggt.
- Wenn Sie eine Sammlung von Entitäten über eine one-to-many oder eine many-to-many Assoziation laden, gibt Hibernate eine Instanz einer Klasse zurück, die die PersistentCollection Schnittstelle implementiert, wie PersistentSet oder PersistentMap. Wenn das erste Mal auf diese Sammlung zugegriffen wird, werden ihre Mitglieder geladen. Die Mitgliedsentitäten werden als reguläre Klassen geladen, so dass die oben erwähnten Fallstricke des Hibernate-Proxys hier nicht zutreffen.
Um ein Gefühl dafür zu bekommen, was hier passiert, sollten Sie einen einfachen JPA-Code im Debugger durchgehen und die Objekte sehen, die Hibernate erstellt. Das wird Ihr Verständnis für den Mechanismus erheblich verbessern. :-)
Bytecode-Instrumentierung zur Laufzeit mit OpenJPA
OpenJPA bietet eine Reihe von Erweiterungsmethodenvon denen ich die Laufzeit-Bytecode-Instrumentierung als die am einfachsten einzurichtende empfand.
Wenn Sie durch den Debugger gehen, können Sie sehen, dass OpenJPA keine Proxies erstellt. Stattdessen sind in jeder Entitätsklasse ein paar zusätzliche Felder mit Namen wie
OpenJPA vs. Hibernate
Der erste Unterschied zwischen diesen beiden Ansätzen besteht in den Objekten, die als Proxies/Instrumente verwendet werden:
- OpenJPA instrumentiert alle Entitäten, d.h. es kann erkennen, wenn Sie von der verweisenden Entität aus auf eine Lazy-Referenz oder eine Sammlung zugreifen und gibt dann eine tatsächliche Entität oder eine Sammlung tatsächlicher Entitäten zurück. Nur wenn Sie eine Entität mit EntityManager.getReference lazy laden oder wenn Sie eine Eigenschaft so konfiguriert haben, dass sie lazy geladen wird, erhalten Sie eine (teilweise) leere Entität.
- Im Falle einer lazy reference (oder einer Entität, die mit EntityManager.getReference lazy geladen wurde) proxyt Hibernate das lazy-Objekt selbst mit CGLIB, was die bereits erwähnten Proxy-Fallen verursacht. Bei der Verwendung einer Lazy Collection ist Hibernate genau so transparent wie OpenJPA. Und schließlich unterstützt Hibernate keine über Proxies geladenen lazy properties.
Wenn Sie die Instrumentierung von OpenJPA mit den von Hibernate erstellten Laufzeit-Proxies vergleichen, können Sie feststellen, dass der Ansatz von OpenJPA transparenter ist. Leider wird er durch die nicht sehr robuste Fehlerbehandlung von OpenJPA etwas enttäuscht.
Das Muster
Da wir nun wissen, wie wir Lazy Loading konfigurieren können und wie es funktioniert, stellt sich die Frage, wie wir es richtig nutzen können.
Schritt 1 besteht darin, alle Ihre Assoziationen zu untersuchen und festzustellen, welche davon "lazily loaded" und welche "eagerly loaded" sein sollten. Als Faustregel gilt, dass ich zunächst alle -zu-eins-Verknüpfungen eifrig (die Standardeinstellung). Normalerweise summieren sie sich ohnehin nicht zu einer großen Anzahl von Abfragen und falls doch, kann ich sie ändern. Dann untersuche ich alle -to-many Verknüpfungen. Wenn es sich dabei um Entitäten handelt, auf die immer zugegriffen wird und die daher immer geladen werden, konfiguriere ich sie so, dass sie eifrig geladen werden. Und manchmal verwende ich die Hibernate-spezifische @CollectionOfElements-Annotation, um solche "wertartigen" Entitäten zuzuordnen.
Schritt 2 ist der wichtigste. Um
- Der reinste Weg ist, eine Service-Fassade (oder Remote-Fassade, wenn Sie so wollen) vor Ihre Dienste zu stellen und mit den Clients Ihrer Service-Fassade nur über Transfer-Objekte (auch bekannt als Data Transfer Objects oder DTOs) zu kommunizieren. Die Fassade ist dafür verantwortlich, alle geeigneten Werte aus Ihren Domänenobjekten in die Datenübertragungsobjekte zu kopieren, einschließlich der Erstellung von Tiefenkopien von Referenzen und Sammlungen. Der Transaktionsbereich Ihrer Anwendung sollte die Service-Fassade einschließen, damit dieses Muster funktioniert, d.h. setzen Sie Ihre Fassade auf @Transaktional oder geben Sie ihr ein eigenes @TransactionAttribute.
- Wenn Sie ein Model 2 Webanwendung mit einem MVC-Framework schreiben, ist eine weit verbreitete Alternative die Verwendung des offenen EntityManagers im View-Muster. In Spring können Sie einen Servlet-Filter oder einen Web MVC-Interceptor konfigurieren, der den EntityManager öffnet, wenn eine Anfrage eingeht, und ihn offen hält, bis die Anfrage bearbeitet wurde. Das bedeutet, dass dieselbe Transaktion in Ihrem Controller
und in Ihrem View (JSP oder anderweitig) aktiv ist. Puristen mögen zwar argumentieren, dass dies Ihre Präsentationsschicht von Ihren Domänenobjekten abhängig macht, aber für einfache Webanwendungen ist dies ein überzeugender Ansatz.
Schritt 3 besteht darin, die SQL-Protokollierung Ihres JPA-Providers zu aktivieren und einige der Anwendungsfälle Ihrer Anwendungen zu üben. Es ist aufschlussreich, zu sehen, welche Abfragen beim Zugriff auf Entitäten durchgeführt werden. Das SQL-Protokoll kann Ihnen auch Input für Leistungsoptimierungen liefern, so dass Sie die in Schritt 1 getroffenen Entscheidungen noch einmal überprüfen und Ihre Datenbank optimieren können. Letztendlich geht es beim Lazy Loading um Leistung, also vergessen Sie diesen Schritt nicht! Ich hoffe, dieser Blog hat Ihnen einen Einblick gegeben, wie Lazy Loading funktioniert und wie Sie es in Ihrer Anwendung einsetzen können. Im nächsten Blog werde ich tiefer in das Thema der DTO- und Service-Fassaden-Muster eintauchen. Doch bevor ich mich von Ihnen verabschiede, möchte ich mich bei allen bedanken, die zu meinem J-Spring 2009 Vortrag zu diesem Thema gekommen sind. Es hat mir sehr viel Spaß gemacht! Anscheinend fragen sich wirklich viele Leute, wie man JPA effektiv einsetzt, denn ich habe eine Menge Fragen bekommen. Leider ging mir wegen der Fragen die Zeit aus. Das nächste Mal werde ich dem Mädchen mit der Zeitkarte mehr Aufmerksamkeit schenken. Und mein eigenes Wasser mitbringen. ;-) Nochmals vielen Dank, dass Sie da waren! P.S. Weiß jemand, was mit hibernate.org passiert ist? Seit mehr als einer Woche zeigt die Website eine Meldung an, dass sie wegen Wartungsarbeiten nicht erreichbar ist. Eine Liste aller Blogs zu JPA-Implementierungsmustern finden Sie in der Zusammenfassung der JPA-Implementierungsmuster.
Verfasst von
Vincent Partington
Unsere Ideen
Weitere Blogs
Contact



