Suche
Kolumne: EnterpriseTales

Java-EE-Architekturen und JPA: Verwendung des EntityManager im Java-EE-Stack

Arne Limburg, Lars Röwekamp

© Software & Support Media

In der letzten ET-Kolumne haben wir gelernt, wie man mittels JPA Use-Case-bezogen Daten laden kann – oder das Laden bewusst unterdrückt. Das Zauberwort hier heißt „Lazy Loading“. Neben den bereits in JPA 2.0 vorhandenen Möglichkeiten sind wir dabei auch auf das in JPA 2.1 kommende Feature der Fetch bzw. Load Graphs eingegangen. Heute wollen wir einen Schritt weiter gehen und analysieren, wie verschiedene Architekturen den Umgang mit Lazy Loading beeinflussen können und welche Möglichkeiten es im Umkehrschluss gibt, Lazy Loading im gesamten Applikationsstack zu berücksichtigen und sinnvoll verwendbar zu machen.

Der klassische Java-EE-Architekturstack sieht seit Java EE 5 so aus, dass in der Visualisierungsschicht JSF Managed Beans existieren, die sich einer darunterliegenden Businessschicht bedienen, in die wiederum ein JPA EntityManager injiziert wird. In Java EE 6 hat sich diesbezüglich nicht viel geändert, außer dass CDI als übergreifende Dependency-Injection-Strategie dazu kam. JSF Managed Beans können durch CDI Beans ersetzt werden, und wer auf zur Laufzeit weiterhin schwergewichtige EJBs verzichten will, kann auch in der darunterliegenden Schicht CDI einsetzen. Anders herum ist es aber z. B. auch möglich, über die neu hinzugekommene Annotation @Named EJBs direkt in der View-Schicht zu verwenden.

Der „geladene Zustand“ einer Entität

Wie spielt Lazy Loading in dieses Szenario hinein? Wenn Lazy Loading jederzeit funktionieren würde, hätten wir kein Problem und müssten uns keine Gedanken über Architekturen machen, um damit umzugehen. Leider ist das nicht so. Die JPA Spec (hier in der Version 2.0, [1]) sagt in Kapitel 3.2.7, „Detached Entities“, sinngemäß, dass eine Applikation nach dem Ende des Persistence Contexts nur noch auf den „verfügbaren Zustand“ einer Entität zugreifen darf. Der verfügbare Zustand ist dabei die Summe aller Attribute, die nicht (implizit oder explizit) als „Lazy“ markiert sind, zusammen mit der Summe aller Attribute, auf die bereits von der Applikation zugegriffen wurde. Dazu zählen auch alle Beziehungen, die entweder mit FetchType.EAGER markiert sind oder bereits geladen wurden, sei es über eine Query (als direktes Ergebnis oder über ein JOIN FETCH) oder über EntityManager.find. Zwar schreibt die Spezifikation nicht vor, dass auf andere Attribute nicht zugegriffen werden darf; aber Applikationen, die das tun, sind nicht mehr „portable“, d. h. der Persistence-Provider kann nicht mehr ohne Weiteres ausgetauscht werden. Und tatsächlich erlaubt auch nur EclipseLink den Zugriff auf nicht geladene Attribute nach dem Ende des Persistence Contexts, und das auch nur, wenn die Entität zwischendurch nicht serialisiert, also z. B. an eine Remote EJB übergeben wurde [2].

Wann wird der EntityManager geschlossen?

Um sicher zu sein, nicht auf ein Problem zu stoßen, dürfen wir also nur bis zum Ende des Persistence Contexts auf Lazy-Attribute zugreifen. Das Ende des Persistence Contexts tritt durch ein EntityManager.close() ein, das in der Regel vom Container vorgenommen wird. Stellt sich die Frage, wann der EntityManager geschlossen wird. Im Falle des oben beschriebenen Java-EE-Stacks ist die Frage leicht zu beantworten: Ein in eine EJB injizierter EntityManager hat den Scope Transaction, d. h. er wird beim Transaktions-Commit (oder -Rollback) geschlossen, was nach Verlassen der äußersten EJB-Methode der Fall ist. Das heißt also, dass in der oben beschriebenen Architektur bereits in den JSF Beans kein Zugriff auf Lazy-Attribute mehr möglich ist. Möchte man in einer JSF Bean z.  B. die Größe einer Liste feststellen, muss diese bereits eine Schicht darunter geladen worden sein. Wie bereits oben erwähnt, ist es mit Java EE 6 möglich, über @Named EJBs als JSF Beans zu verwenden, was das Problem der Listengröße beheben würde. Allerdings verschiebt sich das Problem dadurch nur. Sobald man in einer Facelets-XHTML-Seite über besagte Liste iterieren möchte und sie nicht vorher geladen hat, steht man vor demselben Problem.

[ header = Seite 2: Der Extended Persistence Context ]

Der Extended Persistence Context

Abhilfe verspricht der Extended Persistence Context. Diesen kann man sich vom Container in eine Stateful Session Bean injizieren lassen, indem man in der @PersistenceContext-Annotation das Attribut type=PersistenceContextType.EXTENDED angibt. Ein solcher EntityManager wird nicht bei Transaktionsende geschlossen, sondern erst, wenn die zugehörige EJB zerstört wird. Das bedeutet, dass der EntityManager auch beim Rendern der Facelets-Seite noch geöffnet und somit Lazy Loading möglich ist.

Ist es das? Ist der Extended Persistence Context mit einer Stateful Session Bean unsere Lösung für eine funktionierende Lazy-Loading-Architektur? Leider nicht ganz. Der Persistence Context im Zusammenspiel mit einer Stateful Session Bean hat ein paar weitere Probleme. Zum einen ist es nicht wünschenswert, dass eine Entität, die während einer (Web-)Session geladen wurde, die ganze Zeit (bis zum Ablauf der Session) im Speicher verbleibt. Dies wäre aber mit einem solchen Extended EntityManager aufgrund seines First-Level-Caches der Fall. Das hätte zwar den Vorteil, dass alle Entitäten nie detached wären. Lazy Loading würde also immer funktionieren. Sogar die JPA-Änderungserkennung würde immer dazu führen, dass bei einem Transaktions-Commit alle Änderungen automatisch in die Datenbank geschrieben würden. Allerdings behält ein solcher EntityManager dazu auch alle Entitäten im Speicher. Der Speicherverbrauch einer normalen Websession kann so in ungeahnte Höhen wachsen.

Wo also der oben erläuterte Transactional EntityManager einen zu kurzen Lebenszyklus hat, hat der Extended EntityManager in dieser Variante einen zu langen. Zusätzlich ist das Verhalten für einen Transaktions-Rollback so definiert, dass alle Entitäten des EntityManagers detached werden. Auch das wäre bei diesem Pattern ein großes Problem. Das Pattern sorgt ja eigentlich dafür, dass Entitäten nie detached sind. Dadurch wäre auch der gesamte Programmcode darauf ausgelegt. Ohne komplizierte Sonderbehandlung bliebe bei einem Transaktions-Rollback daher wohl nur das komplette Zerstören und Neuaufbauen der gesamten (Web-)Session.

Ein weiteres Problem bei diesem Pattern ist das Passivieren der Stateful Session Bean. Weder die EJB Spec noch die JPA Spec erwähnen explizit, ob und wie Stateful Session Beans mit einem Extended EntityManager passiviert werden können. Das Problem ist hier, dass die JPA Spec nicht vorsieht, dass ein EntityManager serialisierbar sein muss, und tatsächlich ist auch nur der Hibernate EntityManager serialisierbar. Ergo funktioniert mit allen anderen Persistence-Providern die Passivierung dieser EJBs nicht, was das Speicherproblem noch verschlimmert und einem zusätzlich jeglicher Failover-Strategien beraubt [3]. Immerhin mit JBoss AS 7 und Hibernate ist dies sauber gelöst: Der Container erkennt automatisch, ob alle Entitäten serialisierbar sind und passiviert dann die EJB bei Bedarf.

Der Request-scoped EntityManager

Da die beiden vorgestellten Container-verwalteten Varianten des EntityManagers ungeeignet sind, bleibt die Variante, den EntityManager selbst zu verwalten. Dank CDI und seinem @Produces-Feature ist es tatsächlich auch möglich, einen selbstverwalteten EntityManager als CDI Bean für Dependency Injection zur Verfügung zu stellen (Listing 1). Und es kommt noch besser: Ein solcher selbstverwalteter EntityManager ist EXTENDED. Er hat also gegenüber dem oben vorgestellten Transactional EntityManager den Vorteil, dass er z. B. auch während des Renderns der Facelet-Seiten geöffnet ist und damit auch dort Lazy Loading zur Verfügung stünde.

Gegenüber dem Extended EntityManager der Stateful Session Bean hat er den Vorteil, dass er nach einem Request wieder geschlossen wird. Das verhindert erstens größere Speicherprobleme. Zweitens führt ein Rollback dadurch nicht zu einem Verlust der gesamten Session, sondern lediglich zu einem „kaputten“ Request.

Entitäten, die länger als einen Request lang benötigt werden, müssen allerdings vor jedem Request mittels EntityManager.merge in den aktuellen EntityManager aufgenommen werden. Dies kann ggf. an zentraler Stelle geschehen. Dabei sollte darauf geachtet werden, dass immer mit dem Rückgabewert der merge-Operation gearbeitet wird, um tatsächlich die managed-Instanz zu verwenden.

Offen bleibt bei diesem Pattern die Transaktionsbehandlung. An geeigneter Stelle muss ein EntityManager.joinTransaction() aufgerufen werden. Dies lässt sich am besten über einen CDI Interceptor umsetzen.

@ApplicationScoped
public class PersistenceUnit {
    
    @Produces
    @ApplicationScoped
    public EntityManagerFactory createEntityManagerFactory() {
        return Persistence.createEntityManagerFactory(...);
    }

    public void closeEntityManagerFactory(@Disposes EntityManagerFactory emf) {
        emf.close();
    }
    
    @Produces
    @RequestScoped
    public EntityManager createEntityManager(EntityManagerFactory emf) {
        return emf.createEntityManager();
    }

    public void closeEntityManager(@Disposes EntityManager em) {
        em.close();
    }
}

[ header = Seite 3: Weitere Scopes? ]

Weitere Scopes?

Das hier gezeigte @RequestScoped-EntityManager-Pattern weckt natürlich Begehrlichkeiten. CDI hat doch noch ein paar mehr, teilweise viel interessantere Scopes, wie z. B. den Conversation-Scope. Ist es dann nicht vielleicht auch möglich, besagten EntityManager @ConversationScoped zu erzeugen? Die Antwort ist leider nein, denn wie bereits geschrieben, ist ein EntityManager per se nicht serialisierbar. Der Conversation-Scope verlangt aber von allen enthaltenen Beans, dass sie serialisierbar sind und überprüft dies auch zur Start-up-Zeit.

Eine Möglichkeit, einen @ConversationScoped EntityManager zu erhalten, gibt es aber dennoch: Mit CDI ist es auch möglich, EJBs mit CDI Scopes zu versehen. Wenn wir also die im vorherigen Abschnitt vorgestellte EJB mit einer @ConversationScoped-Annotation versehen würden und den dort injizierten EntityManager wieder über @Produces zu einer CDI Bean machen würden, hätten wir einen @ConversationScoped EntityManager (Listing 2). Wie bereits erwähnt, funktioniert das aber nur sauber mit Hibernate, weil nur der EntityManager serialisierbar ist.

@Stateful
@ConversationScoped
public class PersistenceUnit {
    
    @Produces
    @RequestScoped
    @PersistenceContext(type = EXTENDED)
    private EntityManager entityManager;
}

[ header = Seite 4: Fazit ]

Fazit

Diese Kolumne hat gezeigt, dass es beim Thema Enterprise-Architekturen und Lazy Loading noch eine große Bandbreite an Verhaltensunterschieden bei einzelnen Persistenzanbietern gibt. Wer sich auf EclipseLink festlegt, der erhält Lazy Loading auch bei geschlossenem EntityManager, jedenfalls solange eine Entität nicht serialisiert und deserialisiert wurde. Wer sich auf den Stack JBoss AS 7 und Hibernate festlegt, kann sich über EntityManager-Passivierung und einen @ConversationScoped EntityManager freuen.

Wer komplett unabhängig von Plattformen und Persistence-Providern sein möchte, dem bleibt – neben einem Transactional EntityManager mit aufwändigem Fetching und Merging – eigentlich nur das hier vorgestellte Pattern des @RequestScoped EntityManagers, mit dem sich in der Praxis gut leben lässt und mit dem man alle Lazy-Loading-Probleme im Normalfall los ist. Das bewahrt den Entwickler natürlich nicht davor, immer mal wieder einen Blick auf die abgesetzten SQL-Statements zu haben, um überflüssige und suboptimale Querys zu identifizieren und durch spezielle Fetching-Strategien anzugehen.

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. A.L.2013-10-01 18:36:43

    Meiner Erfahrung nach hängen sämtliche Probleme rund um Lazy-Loading und detached Entities (und Notloesungen wie die im Artikel beschriebene) ausschliesslich mit der verkorksten Architektur zusammen, die in 1000en Mini-Beispielen zur ach-so-einfachen Programmierung mit JSF/Managed Beans/EJB propagiert werden, nämlich in den Named-Beans außerhalb der Transaktion direkt die JPA-Entities zu verwenden.

    Es mag in den simplesten Beispiel (Rate die Zahl, Mini-Blog, ...) ausreichen, und tatsächlich sieht es unglaubilch toll und einfach aus, wie schnell man mit wenigen Annotationen eine halbwegs lauffähige Software auf die Beine stellen kann. Aber sobald man den Tellerrand der ausgedachten Trivial-Beispiele verläßt und halbwegs komplexe Strukturen modelliert, stößt man auf die Lazy-Loading-Problematik wie im Artikel beschrieben.

    Doch anstatt hier dann das eigentliche Problem anzupacken (= verkorkste Architektur), wird stattdessen einfach noch etwas mehr gefrickelt und Notlösungen gebastelt, wie einen EXTENDED EntityManger zu verwenden oder eben die im Artikel beschriebene CDI-Zauberei. In keinem dieser Artikel wird jedoch erwähnt, dass es vielleicht einfach der falsche Ansatz sein könnte, die JPA-Entities direkt an die View-Schicht (hier: JSF) weiterzureichen, wo dann die Beziehungen der Objekte via Lazy-Loading aufgelöst werden sollen.

    All die Probleme um Lazy-Loading verschwinden wie von alleine, wenn man sich an halbwegs bewährte Architekturmuster hält: Der UI-Layer (= Named Beans) ruft einen für den Use-Case der UI angepassten Service im Application Layer (=EJB) auf, welcher ein an den Use-Case-angepasstes (!) View-Model-Objekt zurückliefert. Dann - und nur dann, wenn dann der Use-Case die Auflösung von Abhängigkeiten zwischen Objekten notwendig macht, werden diese dann innerhalb des Application-Layers transparent und vollkommen automatisch wie in der Spec vorgesehen via Lazy-Loading aufgelöst, ohne dass man irgendwelche Zauberei via CDI oder EXTENDED PCs verwenden müsste: Die Transaktion wird rund um den App-Service geöffnet und geschlossen, der EntityManager ist also noch offen, wenn die Abhängigkeiten aufgelöst werden.

    Es reicht dann ein völlig normaler EntityManager aus.

    Ein View-Model-Objekt und die saubere Trennung zwischen UI und App-Schicht ist auf den 1. Blick etwas Schreibarbeit, ist aber deutlich sauberer und zahlt sich - auch aus anderen Gründen (Refactoring, ...) sehr früh aus. Ausserdem befreit dieser Ansatz einen auf einen Schlag vollständig von sämtlichen Problemen rund um's Lazy-Loading, da Abhängigkeiten dort aufgelöst werden, wo es passieren sollte (im Application Layer und nicht in der UI).

  2. A.L.2013-10-01 18:36:43

    Meiner Erfahrung nach hängen sämtliche Probleme rund um Lazy-Loading und detached Entities (und Notloesungen wie die im Artikel beschriebene) ausschliesslich mit der verkorksten Architektur zusammen, die in 1000en Mini-Beispielen zur ach-so-einfachen Programmierung mit JSF/Managed Beans/EJB propagiert werden, nämlich in den Named-Beans außerhalb der Transaktion direkt die JPA-Entities zu verwenden.

    Es mag in den simplesten Beispiel (Rate die Zahl, Mini-Blog, ...) ausreichen, und tatsächlich sieht es unglaubilch toll und einfach aus, wie schnell man mit wenigen Annotationen eine halbwegs lauffähige Software auf die Beine stellen kann. Aber sobald man den Tellerrand der ausgedachten Trivial-Beispiele verläßt und halbwegs komplexe Strukturen modelliert, stößt man auf die Lazy-Loading-Problematik wie im Artikel beschrieben.

    Doch anstatt hier dann das eigentliche Problem anzupacken (= verkorkste Architektur), wird stattdessen einfach noch etwas mehr gefrickelt und Notlösungen gebastelt, wie einen EXTENDED EntityManger zu verwenden oder eben die im Artikel beschriebene CDI-Zauberei. In keinem dieser Artikel wird jedoch erwähnt, dass es vielleicht einfach der falsche Ansatz sein könnte, die JPA-Entities direkt an die View-Schicht (hier: JSF) weiterzureichen, wo dann die Beziehungen der Objekte via Lazy-Loading aufgelöst werden sollen.

    All die Probleme um Lazy-Loading verschwinden wie von alleine, wenn man sich an halbwegs bewährte Architekturmuster hält: Der UI-Layer (= Named Beans) ruft einen für den Use-Case der UI angepassten Service im Application Layer (=EJB) auf, welcher ein an den Use-Case-angepasstes (!) View-Model-Objekt zurückliefert. Dann - und nur dann, wenn dann der Use-Case die Auflösung von Abhängigkeiten zwischen Objekten notwendig macht, werden diese dann innerhalb des Application-Layers transparent und vollkommen automatisch wie in der Spec vorgesehen via Lazy-Loading aufgelöst, ohne dass man irgendwelche Zauberei via CDI oder EXTENDED PCs verwenden müsste: Die Transaktion wird rund um den App-Service geöffnet und geschlossen, der EntityManager ist also noch offen, wenn die Abhängigkeiten aufgelöst werden.

    Es reicht dann ein völlig normaler EntityManager aus.

    Ein View-Model-Objekt und die saubere Trennung zwischen UI und App-Schicht ist auf den 1. Blick etwas Schreibarbeit, ist aber deutlich sauberer und zahlt sich - auch aus anderen Gründen (Refactoring, ...) sehr früh aus. Ausserdem befreit dieser Ansatz einen auf einen Schlag vollständig von sämtlichen Problemen rund um's Lazy-Loading, da Abhängigkeiten dort aufgelöst werden, wo es passieren sollte (im Application Layer und nicht in der UI).

  3. I.A.2017-03-09 13:14:45

    Bravo A.I.

    .. den Nagel auf den Kopf getroffen. Der erste Kommentar seit langem der die Ursache der Problematik beschreibt anstatt Symptome zu bekämpfen.

  4. J.B.2017-10-31 12:23:55

    Bravo A.I.! Deiner Meinung kann ich mich nur anschließen. Nicht "basteln", sondern "entwickeln": das sollte bei dem Thema die Devise sein.

Schreibe einen Kommentar

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