Suche
Java Persistence API - Tipps & Tricks

Advanced JPA: Persistenztricks für Fortgeschrittene

Marco Schulz

(c) Shutterstock / Profit_Image

 

Aus diversen Projekterfahrungen haben sich beim Einsatz des Java Persistence API für die Entwicklung von Applikationen einige Best Practices herauskristallisiert, die sich als äußerst nützlich erwiesen haben. Die Erweiterung des DAO-Patterns, ein weiterführendes Konzept zum Schreiben von Testfällen und die Möglichkeit, mit Entitätsobjekten eine Vererbung abzubilden, sind Punkte, die an dieser Stelle diskutiert werden.

Da Hibernate [1] eine Implementierung des Java Persistence API (JPA) [2] ist, bauen die vorgestellten Beispiele darauf auf. Neben Hibernate sind weitere Referenzimplementierungen der JPA EclipseLink [3], Apache OpenJPA [4] und Oracle TopLink Essentials [5].

Ein Grund, der zur Verwendung von Hibernate geführt hat, ist die starke Verbreitung in vielen Enterprise-Projekten. Dadurch hat sich eine große Community gebildet. Daraus ergibt sich wiederum eine sehr umfangreiche Dokumentation, die nützliche Informationen zur Verwendung von Hibernate bereithält. Ein anderer Aspekt ist die reibungslose Kombination mit dem Dependency-Injection-(DI-)Framework Spring.

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

Vorsicht ist bei einem Wechsel auf eine andere Major-Version geboten. In aller Regel ist die eine oder andere Anpassung vorzunehmen. Erfahrungsgemäß verändern sich beispielsweise Einstellungen der Konfiguration, die dann durch umfangreiche Tests geprüft werden müssen. Gefahren lauern beispielsweise in unzureichender Performance, was verschiedene Ursachen haben kann. Klassische Fallstrike lauern außerdem oft bei der unglücklichen Verwendung von Lazy oder Eager Loading, unsauber geschlossenen Transaktionen oder übermäßigem Gebrauch eines Commits der Entitäten in die Persistenzschicht. Tabelle 1 zeigt, welche Hibernate-Version das JPA entsprechend umsetzt.

Hibernate-Version JPA-Version JSR
Hibernate 3.2 + JPA 1.0 JSR 220 – 05.2006
Hibernate 3.5 + JPA 2.0 JSR 317 – 12.2009
Hibernate 4.3 + JPA 2.1 JSR 338 – 04.2013

Tabelle 1: Übersicht der Hibernate-Versionen mit dem umgesetzten JPA-Standard [7]

Wenn man sich tiefergehend mit der Thematik befasst, eröffnen sich verschiedene Aspekte, die zu einem genaueren Blick verführen. So lassen sich spezielle Fragen klären, die für manche Projekte einen hervorragenden Lösungsansatz offerieren. So kann beispielsweise das Data-Access-Object-(DAO-)Pattern mit weiteren Funktionalitäten angereichert werden. Eine Einführung in das DAO-Pattern findet sich unter [6].

Extended DAO

Im DAO-Pattern werden grundsätzlich die CRUD-Operationen vorgehalten. Da dies wichtige Basisfunktionen sind, bietet sich diese Position aus architektonischer Sicht für einige nützliche Erweiterungen an. Serialisierung und Deseralisierung der Datenstrukturen nach XML und JSON lassen sich so leicht standardisieren. Um nicht übermäßigen Implementierungsaufwand zu betreiben, sollten diese Services auf Objektebene operieren.

Das bedeutet, dass die konkreten Objekte (Entitäten) an die Funktion gereicht werden. Das gestattet es wiederum, mit wenigen Handgriffen Iteratoren anzulegen, die sich für Export- und Importservices gut eignen. Eine leicht zu verwendende Bibliothek für die Verarbeitung von JSON-Objekten ist Flexjson. Listing 1 zeigt, wie Flexjson als Maven Dependency eingebunden wird, und gibt ein Beispiel für das Serialisieren eines Objekts nach JSON. Um das Beispiel einfach zu halten, wurde bewusst darauf verzichtet das JSON-Objekt direkt aus der Datenbankabfrage zu generieren. Außerdem gestattet diese Implementierung ein wenig mehr Freiheiten, da auch nicht persistente Entitäten serialisiert werden können.

<dependency>
  <groupId>net.sf.flexjson</groupId>
  <artifactId>flexjson</artifactId>
  <version>3.3</version>
</dependency>

public final String serializeAsJson(final T object) {
  String json = "";
  if (object != null) {
    JSONSerializer serializer = new JSONSerializer();
    json = serializer.serialize(object);
  } else {
    LOGGER.log("Can't create JSON String, because the Entity is emty.", Logger.ERROR);
  }
    return json;
}

 

Das Vorgehen für XML verläuft identisch. Dabei sollte auf die bereits im Projekt verwendete XML-Technologie gesetzt werden. Damit ist auch die Verarbeitung per XML projektübergreifend standardisiert. Es ist also nicht förderlich, zur bereits verwendeten Schnittstelle SAX noch einen weiteren Standard wie DOM oder StAX zu etablieren.

Performance optimieren

Im Kontext von JPA existieren einige Begrifflichkeiten, deren Kenntnis hilfreich für ein tieferes Verständnis ist. So kennt der EntityManager beispielsweise die beiden Zustände persistent (nicht flüchtig) und transient (flüchtig). Abbildung 1 zeigt für die Zustände eine einfache Übersicht der entsprechenden Operationen. Detached kennzeichnet Datensätze in der Datenbank, die bereits aus dem Persistenzkontext des EntityManagers gelöst sind, während im Zustand removed der Datensatz aus dem Datenbankmanagementsystem (DBMS) entfernt ist.

JPA Tipps & Tricks

Abb. 1: Der EntityManager kennt beispielsweise die Zustände persistent und transient sowie remove und detach

Ein anderer Aspekt ist die Abwägung zwischen Eager und Lazy Loading. Durch geschickte Kombination lässt sich dadurch einiges an Geschwindigkeit optimieren. Eager Loading (frühes Laden) ist die standardmäßig verwendete Strategie, Entitätsobjekte inklusive aller Werte in den Speicher zu laden. Dem gegenüber steht Lazy Loading (faules Laden, oder müßiges Laden). Mittels des FetchType kann für eine Entität Lazy Loading aktiviert werden. Ein Beispiel:

 

@OneToOne(fetch = FetchType.LAZY)

 

In diesem Fall werden die Inhalte der Objekte erst bei Bedarf ermittelt. Ein anschaulicher Vergleich dazu ist das Laden von Grafiken in eine HTML-Seite. Bei Eager werden sämtliche Grafiken geladen, bevor die HTML-Seite gerendert wird, während Lazy nur die Grafiken lädt, die auch tatsächlich auf der zu rendernden Seite angezeigt werden. Die Risiken, die sich durch die Nutzung des Lazy Loadings ergeben, entstehen vor allem bei Strukturen, die viele Einzelabfragen erzeugen und dadurch einen starken Overhead verursachen. Wenn bereits von Beginn an klar ist, dass die gesamte Objektstruktur geladen werden muss, verursacht Lazy Loading unnötigen Overhead, da auf Proxies zugegriffen werden muss. Das ist für Eager Loading nicht notwendig.

Ein weiterer Aspekt der Performanceoptimierung ist das Caching von Datenobjekten. Auch dieser Bereich offeriert mehrere Möglichkeiten, aus denen es gilt, die optimale Lösung für die eigene Applikation auszuwählen. Im Second Level Cache (L2) werden Entitäten anhand ihres Primärschlüssels vorgehalten. Kombiniert man den L2 Cache mit Lazy Loading, kann ein Query Cache erzeugt werden. Dieser beantwortet identische Abfragen mit derselben Ergebnismenge, ohne erneut das DBMS zu konsultieren. Alternativ kann auch ein eigener Cache für spezialisierte Abfragen zum Einsatz kommen.

Eindeutigkeit hilft beim DBMS-Wechsel

Ein gewichtiger Vorteil der Verwendung von Hibernate ist, dass sich das darunterliegende DBMS austauschen lässt. Aber wie üblich, liegt auch hier der Fehlerteufel im Detail. Der Erfolg eines leicht zu bewerkstelligenden Wechsels des DBMS ist von einigen Faktoren abhängig, die es zu beachten gilt. So ist das verwendete AUTO_INCREMENT von MySQL als Primärschlüssel selten eine gute Wahl, da sich dieses Konstrukt in anderen DBMS wie PostgreSQL nicht ohne Weiteres nutzten lässt. Stellt man sich z. B. das Szenario eines Redaktionssystems vor, in dem der Primary Key für Beiträge als AUTO_INCREMENT vergeben wird, erkennt man schnell die offensichtlichen Mängel dieser Strategie.

Geht man davon aus, dass ein durch einen Workflow freigegebener Beitrag zum Publizieren in einem anderen Schemata gespeichert wird, so erhält dieser Beitrag einen neuen Index. Die Lösung dieser Problematik ist die Verwendung eines Universally Unique Identifier (UUID) [8]. Die Vergabe dieses UUID wird durch die Applikation gesteuert und ist über verteilte DBMS stets gleich. Dadurch ist sichergestellt, dass gleiche Datensätze immer denselben Index verwenden. Diese Überlegung hat auch positive Auswirkungen bei der Verwendung einer Volltextsuche. Eine Möglichkeit unter Java einen UUID zu erzeugen, ist die Verwendung der Klasse java.util.UUID [9].

Mehr zum Thema: JPA: Architekturkonzepte und Best Practices revisited

Bei näherer Betrachtung der vorangegangenen Überlegungen stellt sich die berechtigte Frage, ob es möglich ist, unter Hibernate verschiedene DBMS innerhalb einer Applikation zu verwenden. Die Antwort ist ein klares Ja. Es müssen dafür aber verschiedene DataSources und EntityManager konfiguriert werden. Es ist auch zu beachten, dass jede DataSource eine eigene Zugriffslogik benötigt. Typische Gründe für die Verwendung unterschiedlicher DBMS sind beispielsweise eine Kombination mit NoSQL oder Schemata für geografische Informationssysteme (GIS). In diesen Fällen unterscheidet sich auch die Technik der verwendeten Zugriffsart. So ist das DAO-Pattern in der vorgestellten Form für GIS oder NoSQL eher ungeeignet.

Vererbung bei Entity-Objekten nutzen

Eine etwas weniger beachtete Option sind die verschiedenen Vererbungsstrategien von Entity-Objekten. Diese Vererbungsmechanismen haben direkte Auswirkungen auf das Erzeugen der entsprechenden Tabellen. JPA unterstützt dazu drei verschiedene Varianten: single table, joined und table per class. Single table ist das Standardverhalten und bedeutet, dass aus einem Vererbungsbaum mit einer Elternklasse und zwei Kindern eine gemeinsame Tabelle erzeugt wird. Alle Attribute der Klassen werden in einer gemeinsamen Tabelle zusammengefügt. Die Variante joined bildet hingegen eine Eins-zu-eins-Beziehung zwischen Eltern- und Kindklasse ab.

Die Verknüpfung erfolgt in diesem Fall über Fremdschlüsselbeziehungen. Als abschließende Möglichkeit gibt es noch den Fall one table per class. Diese verhältnismäßig langsame Variante erzeugt für jede Kindklasse des Objektbaums eine eigene Tabelle und fügt dieser die Attribute der Elternklasse hinzu.

Um die Unterschiede der drei verschiedenen Ausprägungen besser illustrieren zu können, soll als einfaches Beispiel die Spezialisierung der Elternklasse BankAccount mit den Attributen id und amount dienen. Als direkte Kinder sollen die Klassen GiroKonto und TagesgeldKonto abgeleitet werden. Die spezielle Eigenschaft des Tagesgeldkontos ist der Zinssatz, wo hingegen für das Girokonto als Attribut der Überziehungsrahmen möglich sein soll. (Abb. 2).

JPA Tipps & Tricks

Abb. 2: Aus der Objektstruktur wird die passende Tabelle erzeugt

 

Auch wenn das JPA die Möglichkeiten der Vererbung vorsieht, sollte beim Domainentwurf möglichst auf einfache Strukturen geachtet werden. Es ist leicht einzusehen, dass eine hohe Komplexität schwerer zu beherrschen ist und sich mit aller Wahrscheinlichkeit auch suboptimal auf die Performance auswirkt. So stellt sich die Frage: Wann kann die Möglichkeit der Vererbung gewinnbringend eingesetzt werden? Die Antwort ist in der Modularisierung von Komponenten zu finden. So lässt sich eine Basisfunktion auf diese Art in einem eigenen Package erweitern, ohne dass schwerwiegende Änderungen am Datenmodell notwendig werden.

Testfälle genau benennen und spezifisch halten

Die Erkenntnis, dass das Testen von Applikationen zwar ein gewisses Verhalten sicherstellt, aber keine Fehlerfreiheit der Anwendung garantiert, trifft bei Entitäten des JPA besonders zu. Um den Sachverhalt ein wenig deutlicher zu skizzieren, sind ein paar Überlegungen zur Gestaltung von Testfällen nützlich. Die wichtigste Anforderung an einen Test ist eine klare Aussagekraft. Dies ist am leichtesten zu erreichen, wenn der Testfall eine sprechende Bezeichnung hat. Um wiederum ohne große Mühe eine gute Benennung finden zu können, sollte der Testfall möglichst einfach gestaltet sein. Im Konkreten bedeutet dies, dass Methoden, die verschiedenen Bedingungen unterliegen, pro Testfall lediglich eine einzige Bedingung prüfen sollten.

Auch wenn es im ersten Moment nach höherem Aufwand aussieht, ist der Ertrag dieser Bemühung enorm. Einerseits gewinnt man während der Entwicklung durch die feine Granularität eine höhere Flexibilität bei der Pflege der Testfälle. Aber auch die Erkenntnis über das vorhandene Problem bei der Fehlersuche während eines Fehlschlags bei einem Testdurchlauf ist beachtlich. Unter Berücksichtigung des empfohlenen Testdesigns ist es nicht notwendig, bei Fehlschlägen den Test zu konsolidieren, um das tatsächliche Problem zu identifizieren.

In Bezug auf Entitätsobjekte gibt es verschiedene Aspekte, für die Tests motiviert sind. Über das Tool Bean Matchers [10] lassen sich mit wenig Aufwand die Konstruktoren sowie Getter und Setter auf richtige Funktionsweise hin überprüfen. Diese Maßnahme beschert bereits eine hundertprozentige Testabdeckung bei einer Überprüfung durch Cobertura [11]. Leider berücksichtigt die Test-Coverage keine Annotationen, wie sie durch den Validation-Standard von Java verwendet werden. Exakt dieser Aspekt ist aber der Mittelpunkt unserer Bemühungen für Tests. Es bleibt also nichts Müßigeres übrig, als für jede Validation-Annotation eines Attributs einen entsprechenden Testfall bereitzustellen und auch die Existenz eines solchen regelmäßig zu überprüfen. Listing 2 zeigt eine vollständige Testroutine.

00: @Test
01: public void testDomainObject() {
02: assertThat(Configuration.class, hasValidBeanConstructor());
03: assertThat(Configuration.class, hasValidGettersAndSetters());
04: assertThat(Configuration.class, hasValidBeanToString());
05: assertThat(Configuration.class, hasValidBeanHashCodeFor("key", "modulName", "version"));
06: assertThat(Configuration.class, hasValidBeanEqualsFor("key", "modulName", "version"));
07: }
08:
09: 

 

Wie die Methode testDomainObject zeigt, durchlaufen Getter, Setter, Konstruktoren sowie equals() und toString() die von Bean Matchers bereitgestellten Testroutinen. Eine ausführliche Beschreibung der Möglichkeiten findet sich auf der Projekthomepage.

Fazit

Wie Sie bereits zu Recht vermuten, ist mit diesem Artikel die Thematik des JPA nicht erschöpfend abgeschlossen. Das Hauptaugenmerk habe ich vor allem auf Themen gelegt, die den Entwickleralltag erleichtern, aber nicht so populär sind, wie das Formulieren von Criteria-Abfragen, die wegen des täglichen Gebrauchs eine stärkere Sichtbarkeit haben. Dazu sei aber auch auf die umfangreich vorhandene Literatur verwiesen, die für JPA und auch besonders Hibernate existieren. Aber auch die Onlinedokumentationen der entsprechenden Hersteller sind zu einer Konsultation zu empfehlen.

Mehr zum Thema:

Zehn Jahre JPA: Von einem (Standard), der auszog, Veränderung einzuleiten

 

Geschrieben von
Marco Schulz
Marco Schulz
Marco Schulz studierte an der HS Merseburg Diplominformatik. Sein persönlicher Schwerpunkt liegt in der Automatisierung von Build-Prozessen und dem Softwarekonfigurationsmanagement. Seit über zehn Jahren entwickelt er auf unterschiedlichen Plattformen Webapplikationen. Derzeit arbeitet er als freier Consultant und ist Autor verschiedener Fachartikel.
Kommentare
  1. TestP2017-05-05 15:05:03

    Fetchtype.EAGER ist potentiell beträchtlich gefährlicher für die Performance als es hier dargestellt wird. Das Beispiel mit dem Bildern ist für eine Website sicherlich nicht gefährlich. Aufpassen sollte man da aber in ERP-Systemen. Dort sind Tabellen oftmals mehrfach kaskadierend miteinander verkettet und mit Fetchtype.EAGER kann man sehr schnell die halbe Datenbank im RAM haben bloß weil man einen Kunden geöffnet hat. Der Kunde lädt die Adresse, die Adresse lädt die Stadt, die Stadt lädt alle Strassen, die Strassen laden alle Hausnummern, die Hausnummern laden Geokoordinaten etc. Und schon hat man einen Multi-GB-BLOB im RAM.

    Featchtype.EAGER würde ich unter keinen Umständen einsetzen. Wenn ich so ein Verhalten wirklich brauche dann nehme ich einen EntityGraph und wähle hier die Tabellen aus, die ich kontrolliert vorausschauend laden will. Ja, das erfordert eine individuelle Abfrage für den Usecase.

    Grüße

Schreibe einen Kommentar

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