Kolumne: EnterpriseTales

Was du später kannst besorgen: Lazy Loading in JPA 2.1

Arne Limburg, Lars Röwekamp

© Software & Support Media

In der letzten Kolumne haben wir einen Blick über den Java-EE-Standard-Tellerrand hinaus geworfen und das alternative Query-API „QueryDSL“ vorgestellt. Heute kehren wir wieder zurück in den JPA-Standard. Dort gibt es seit dem 22.05.2013 das „Final Release“ der Version 2.1. Und dieses bringt ein interessantes neues Feature im Bereich Lazy Loading mit sich. Bei diesem neuen JPA-Feature geht es darum, Daten erst zu einem späteren Zeitpunkt zu „besorgen“, nämlich dann, wenn sie tatsächlich gebraucht werden. Dass dieses Feature auch seine Tücken hat und welche Neuerung JPA 2.1 hier bringt, wollen wir im Folgenden zeigen.

Jeder erfahrene Java-EE-Entwickler kennt das Problem: In der klassischen Java-EE-Schichtenarchitektur besteht die Serviceschicht aus EJBs. In diese wird via @PersistenceContext ein EntityManager injiziert. Die EJB-Schicht stellt gleichzeitig die Transaktionsgrenze dar, was bedeutet, dass der EntityManager standardmäßig nach Verlassen der EJB-Schicht geschlossen wird. Nach dem Schließen funktioniert das Nachladen von Beziehungen, das so genannte Lazy Loading, aber nicht mehr. Dies führt zu hässlichen Exceptions, wenn man z. B. in der Facelets-Seite über eine Collection einer Entity iterieren will (Listing 1 und 2), die nicht bereits in der EJB-Schicht geladen wurde.

Angular - Eine Einführung

Angular – eine Einführung

Manfred Steyer

In diesem Videotutorial erklärt Manfred Steyer alles, was man für einen professionellen Umgang mit Angular benötigt und zeigt anhand eines praxisnahen Beispiels, wie man Services und Dependency Injection integriert, Pipes zur Formatierung von Ausgaben nutzt, Komponenten mit Bindings verwendet, Angular-Anwendungen mit Modulen strukturiert und Routing gebraucht.

Wie kann aber sichergestellt werden, dass die benötigte Beziehung bereits in der EJB-Schicht geladen wurde? Der JPA-Standard 2.0 bietet hier zwei Möglichkeiten: Eager Fetching im Mapping oder Fetching über die Query. Eine dritte Möglichkeit, nämlich, dass innerhalb der Transaktionsgrenze „manuell“ auf die benötigten Attribute zugegriffen wird, um ein Laden zu forcieren, wird hier nicht näher betrachtet.

Eager Fetching im Mapping

JPA bietet die Möglichkeit, im Mapping anzugeben, ob eine Beziehung gleich mitgeladen werden soll, wenn die eigentliche Entität geladen wird. Dies ist möglich, indem man im Mapping den FetchType.EAGER angibt. Dieser ist bei To-one-Beziehungen (also @ManyToOne und @OneToOne) der Default. Bei To-many-Beziehungen (also @OneToMany und @ManyToMany) dagegen muss er explizit angegeben werden. In Listing 1 würde dies bedeuten, dass dank expliziter Angabe des Fetch-Typs beim Laden der Person automatisch auch die Adressen mitgeladen würden.

@Entity
public class Person {
    ...
    @OneToMany(fetch = FetchType.EAGER)
    private List<Address> addresses;
    ...
}
@Entity
@NamedEntityGraph(
    name = "Person.addresses", 
    attributeNodes = @NamedAttributeNode("addresses")
)
public class Person {
    ...
    @OneToMany(fetch = FetchType.LAZY) // default fetch type
    private List<Address> addresses;
    ...
}

Die Angabe von FetchType.EAGER im Mapping hat allerdings auch Nachteile. Zum einen sollte diese Variante auf keinen Fall an jeder Beziehung angegeben werden. Das würde zwar das Lazy Loading inkl. der damit einhergehenden, oben beschriebenen Probleme komplett ausschalten. Es würde aber auch dazu führen, dass man beim Laden einer Entität potenziell den gesamten Inhalt der Datenbank in den Speicher lädt, nämlich genau dann, wenn alle Entitäten miteinander verbunden sind. Dies ist sowohl vom Speicherbedarf als auch von der Menge der Datenbankoperationen her eine mittlere bis schwerwiegende Katastrophe.

Aber selbst, wenn man mit der Angabe von FetchType.EAGER gezielt umgeht und es nur an solchen Beziehungen angibt, wo ein gemeinsames Laden Sinn macht, hat die Angabe den Nachteil, dass sich immer Use Cases finden lassen (und sei es in ferner Zukunft), in denen man die Objekte nicht gemeinsam laden will. Einen einmal eingeführten FetchType.EAGER wird man in der Praxis nur schwer wieder los, da sich die Auswirkungen auf die anderen Programmteile nicht absehen lassen (wohl dem, der dann ausreichend Unit Tests geschrieben hat).

FetchType.EAGER hat einen weiteren unerwünschten Nebeneffekt: Im JPA-Standard ist nicht spezifiziert mit wie vielen SELECTs die Abhängigkeiten geladen werden sollen. Man hat hier also Anzahl und Umfang der Datenbankoperationen nicht im Griff, was zu einem weiteren „berüchtigten“ JPA-Problem führen kann: den n+1 SELECTs. Beim Iterieren über eine Liste kann es dadurch vorkommen, dass zunächst ein SELECT ausgeführt wird, um die Liste (lazy) zu laden und dann n weitere, um die Entitäten, die EAGER an den Listeneinträgen hängen, nachzuladen. n entspricht dabei der Größe der Liste und kann damit potenziell recht groß werden. Einige JPA-Anbieter erkennen das n+1-Problem mittlerweile und führen dann nur ein weiteres SELECT für die anhängenden Entitäten aus. Diese Optimierung ist aber längst nicht bei allen Anbietern umgesetzt.

Fetching über die Query

Eine weitere Möglichkeit zu beeinflussen, welche Teile meines Entity-Graphen geladen werden sollen, bieten die Abfragesprachen von JPA. Sowohl in der JPQL (Java Persistence Query Language) als auch in dem Criteria-API ist es möglich, explizit anzugeben, welche Daten direkt mitgeladen werden sollen. Hiermit kann man sogar direkt steuern, wie viele Abfragen zum Laden abgesetzt werden.

Ein Nachteil des Fetchings über die Query ist, dass man Use-Case-bezogene Queries schreiben muss. Man kann sich vom EntityManager z. B. nicht direkt die Person mit der ID 5 geben lassen, sondern muss, je nach Use Case eine andere Query absetzen, um den korrekten Entity-Graphen mitzuladen. Das ist ärgerlich, weil auf diese Weise eine ganze Reihe Use-Case-spezifischer Abfragen entstehen können, die eigentlich dasselbe tun, nämlich eine Entity mit einer bestimmten ID zu laden.

Beim Laden einer Entity zu einer konkreten ID mag sich der Ärger noch in Grenzen halten, aber spätestens, wenn man komplexe Abfragen (z. B. Suchabfragen) mit einer komplizierten WHERE Clause und mehreren Parametern benötigt, möchte man diese im Quellcode nicht kopieren müssen, nur um Use-Case-bezogen verschiedene Daten mitzuladen.

Entity Graphs

Genau hier setzt das neue Feature von JPA 2.1 ein, die Entity Graphs. Die Idee ist es, die Angabe der Fetch/Load Graphs von der Abfrage zu trennen, um sich in der Abfrage auf die Logik zu konzentrieren und bei der Angabe der Fetch/Load Graphs auf die Spezifikation, welche Daten geladen werden sollen.

Dabei wird es mehrere Möglichkeiten geben, einen solchen Entity Graph zu erstellen. Die erste Möglichkeit ist, den Entity Graph via Annotation an der Entity selbst anzugeben (Listing 2) und ihn dann später über seinen Namen zu referenzieren, z. B. in einer find-Operation oder in einer Query (Listing 3). Hierbei wird zwischen den Properties javax.persistence.fetchgraph und javax.persistence.loadgraph unterschieden. Der Unterschied beider Properties liegt dabei in der Behandlung von Attributen, die nicht im jeweiligen Entity-Graphen angegeben sind: Während beim Fetch-Graphen alle nicht angegebenen Attribute automatisch lazy sind, behalten sie beim Load-Graphen ihr im Mapping konfiguriertes Fetch-Verhalten und werden, falls sie als EAGER gemappt sind, zusätzlich zu den im Load-Graphen angegebenen Attributen geladen.

EntityManager entityManager  = ...
EntityGraph entityGraph = entityManager.getEntityGraph("Person.addresses");

Map<String, Object> hints = new HashMap<String, Object>();
hints.put("javax.persistence.fetchgraph", entityGraph);
Person person = entityManager.find(Person.class, id, hints);

TypedQuery<Person> query
    = entityManager.createNamedQuery("Person.findAll", Person.class);
query.setHint("javax.persistence.loadgraph", entityGraph);
List<Person> persons = query.getResultList();

Die zweite Möglichkeit, einen Entity-Graphen zu erstellen, ist, dieses dynamisch im Code zu tun. Hierfür bietet JPA 2.1 ein eigenes API. Über EntityManager.createEntityGraph kann ein neuer Entity Graph erzeugt werden. Dieser kann dann im Nachgang durch das Hinzufügen von Attributen und Sub-Graphen mit Leben gefüllt werden (Listing 4). Ein so erzeugter Entity-Graph kann sogar in der EntityManagerFactory über addNamedEntityGraph registriert werden und muss so im weiteren Programmverlauf nicht immer wieder neu erzeugt werden.

EntityManager entityManager  = ...
EntityGraph entityGraph = entityManager.createEntityGraph(Person.class);
entityGraph.addAttributeNodes("addresses");

Map<String, Object> hints = new HashMap<String, Object>();
hints.put("javax.persistence.fetchgraph", entityGraph);
Person person = entityManager.find(Person.class, id, hints);

EntityManagerFactory factory = entityManager.getEntityManagerFactory();
factory.addNamedEntityGraph("Person.addresses", entityGraph);

Fazit

Lazy Loading ist nach wie vor ein immer wiederkehrendes Problem für Entwickler, die JPA zum Mapping von Objektmodell und relationaler Datenbank verwenden. Mit der Version 2.1 wird eine neue Möglichkeit eingeführt, Use-Case-bezogen zu definieren, welche Entitäten mit einer Datenbankabfrage in den Speicher geladen werden sollen. Dieses Feature der Entity-Graphen wurde hier näher vorgestellt. Der große Vorteil von Entity-Graphen ist es, dass sie es ermöglichen, Lazy Loading pro Use Case zu spezifizieren und das ggf. schon in der Serviceschicht. Auf diese Weise wird eine Use-Case-unabhängige Datenzugriffsschicht ermöglicht.

Neben dem Standardisieren von DB-Schemageneration sind die Entity-Graphen sicher das große neue Feature in JPA 2.1. Mit ihnen wird ein weiteres Feature, das bisher nur in verschiedenen Persistenzanbietern proprietär vorhanden war, in den Standard aufgenommen.

Geschrieben von
Arne Limburg
Arne Limburg
Arne Limburg ist Softwarearchitekt bei der open knowledge GmbH in Oldenburg. Er verfügt über langjährige Erfahrung als Entwickler, Architekt und Consultant im Java-Umfeld und ist auch seit der ersten Stunde im Android-Umfeld aktiv.
Lars Röwekamp
Lars Röwekamp
Lars Röwekamp ist Gründer des IT-Beratungs- und Entwicklungsunternehmens open knowledge GmbH, beschäftigt sich im Rahmen seiner Tätigkeit als „CIO New Technologies“ mit der eingehenden Analyse und Bewertung neuer Software- und Technologietrends. Ein besonderer Schwerpunkt seiner Arbeit liegt derzeit in den Bereichen Enterprise und Mobile Computing, wobei neben Design- und Architekturfragen insbesondere die Real-Life-Aspekte im Fokus seiner Betrachtung stehen. Lars Röwekamp, Autor mehrerer Fachartikel und -bücher, beschäftigt sich seit der Geburtsstunde von Java mit dieser Programmiersprache, wobei er einen Großteil seiner praktischen Erfahrungen im Rahmen großer internationaler Projekte sammeln konnte.
Kommentare
  1. Oliver Weiler2017-06-14 15:00:09

    Super Artikel, ich habe bisher ausschließlich fetch joins verwendet, die Verwendung von Entity Graphs erscheint mir allerdings als die deutlich elegantere Alternative (in einigen Fällen).

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.