Datenbankabfragen mit JPA – mehr als nur em.find und JPQL

Datenbankabfragen mit JPA sind nicht nur mit EntityManager und JPQL möglich. Criteria API, native SQL-Abfragen und der Aufruf einer Stored Procedure sind manchmal viel besser geeignet und sollten daher fester Bestandteil im Werkzeugkasten der Programmierer sein.

Know-how  –  5 Kommentare
Datenbankabfragen mit JPA – mehr als nur em.find und JPQL

Wer von JPA und Datenbankabfragen spricht, denkt meist gleich an den EntityManager oder die Java Persistence Query Language (JPQL). Das sind die mit Abstand bekanntesten Möglichkeiten, Datensätze mittels JPA aus einer Datenbank zu laden. Darüber hinaus bietet die Spezifikation noch einige weitere Möglichkeiten, wie die Criteria API, native SQL-Abfragen oder den Aufruf einer Stored Procedure. Je nach Anwendungsfall sind diese deutlich besser geeignet als die bekannteren Alternativen.

Den Anfang macht die find-Methode des EntityManager. Sie erlaubt es, Entitäten anhand ihres Primärschlüssels zu laden. Intern generiert der Persistenz-Provider, meist Hibernate, EclipseLink oder OpenJPA, dafür eine SQL-Abfrage, die den Datensatz mit einem gegebenen Primärschlüssel aus der entsprechenden Tabelle lädt.

Author a = em.find(Author.class, 1L);

Das ist der mit Abstand einfachste Weg, eine Entität aus der Datenbank zu laden. Es sind lediglich die Klasse der Entität und der Wert des Primärschlüssels als Parameter an die find-Methode zu übergeben. Alle weiteren Schritte übernimmt die durch den Persistenz-Provider zur Verfügung gestellte Implementierung des EntityManager-Interfaces.

Es gibt jedoch auch einige Nachteile:

  • Die zu ladende Entität lässt sich ausschließlich über den vollständigen Primärschlüssel identifizieren. Andere Eigenschaften oder Teile eines zusammengesetzten Primärschlüssels können nicht verwendet werden.
  • Der Persistenz-Provider führt für jeden Aufruf der find-Methode eine Datenbankabfrage aus, die maximal einen Datensatz selektiert.

Die find-Methode eignet sich daher ausschließlich für Anwendungsfälle, in denen nur eine geringe Anzahl von Entitäten anhand ihres Primärschlüssels geladen werden sollen.

Einige Persistenz-Provider erweitern das EntityManager-Interface um zusätzliche, proprietäre Methoden. Die Session-Implementierung von Hibernate bietet zum Beispiel die Möglichkeit, mehrere Entitäten anhand ihres Primärschlüssels oder einzelne Entitäten mithilfe ihres natürlichen Schlüssels zu laden.

JPQL ist deutlich flexibler als die Methoden des EntityManager und ähnelt in der Syntax dem von relationalen Datenbanken verwendeten SQL. Der Hauptunterschied besteht darin, dass SQL-Abfragen Datenbankschemata, -tabellen und -spalten referenzieren. JPQL-Abfragen hingegen werden auf Basis des Domänenmodells und der darin festgelegten Entitäten und ihrer Relationen definiert. Zur Ausführungszeit nutzt der Persistenz-Provider dann die Mapping-Definitionen des Modells, um für die JPQL- eine SQL-Abfrage zu generieren.

Das folgende Beispiel zeigt eine Ad-hoc-JPQL-Abfrage, mit der alle Autoren selektiert werden, die ein Buch geschrieben haben, in dessen Titel das Wort "Java" vorkommt.

List<Author> authors = em.createQuery("SELECT a FROM Author a 
JOIN a.books b WHERE b.title LIKE '%Java%'", Author.class).getResultList();

Ad-hoc-Abfragen werden vom Persistenz-Provider zur Laufzeit erzeugt und ausgeführt. Dieselbe Abfrage lässt sich auch als @NamedQuery mit einer Annotation definieren. Der Provider kann sie während des Deployments der Anwendung prüfen und für die Ausführung vorbereiten.

@Entity
@NamedQuery(name="Author.selectAuthorsOfBook", query="SELECT a FROM Author
a JOIN FETCH a.books b WHERE b.title LIKE '%JAVA%'")
public class Author {...}

In der Geschäftslogik lässt sich die @NamedQuery über ihren Namen referenzieren und ausführen:

List<Author> authors = em.createNamedQuery("Author.selectAuthorsOfBook",
Author.class).getResultList();

Auch wenn sich JPQL und SQL ähneln, werden die Unterschiede schnell deutlich, wenn man dieselbe Abfrage mit SQL formuliert.

SELECT a.id, a.firstName, a.lastName, a.version FROM Author a JOIN 
BookAuthor ba ON a.id=ba.authorId JOIN Book b ON ba.bookId=b.id
WHEREb.title LIKE '%Java%'

Die JPQL-Abfrage verwendet die Author-Entität als Projektion. Das referenziert implizit alle Eigenschaften der Entität, die anschließend mithilfe der Mapping-Definitionen auf Datenbanktabellen und -spalten abgebildet werden. Im Gegensatz dazu sind bei SQL alle zu selektierenden Spalten explizit zu benennen.

Des Weiteren ist die Definition einer JOIN-Klausel in JPQL deutlich einfacher, da diese lediglich eine bereits definierte Beziehung referenziert. Im obigen Beispiel wird die books-Eigenschaft der Author-Entität verwendet. Sie bildet die Many-to-Many-Assoziation zwischen der Author- und der Book-Entität ab. Der Persistenz-Provider erhält die benötigten Informationen über die verwendeten Fremdschlüssel und die Assoziationstabelle aus den Mapping-Definitionen der beiden Entitäten. Daher sind sie in der Abfrage nicht zu benennen. Bei SQL ist das hingegen erforderlich, wodurch die Definition der Abfrage komplizierter wird.

Viele Entwickler bevorzugen daher JPQL und die damit verbundene Abstraktion der Datenbankschicht. Allerdings hat das auch Nachteile. Die Details der ausgeführten SQL-Abfrage sind durch die Abstraktion häufig nur schwer vorherzusagen. Daher sollten während der Entwicklung alle ausgeführten Datenbankabfragen durch den Persistenz-Provider geloggt und seitens der Entwickler geprüft werden. Dadurch lassen sich ineffiziente Datenbankzugriffe und daraus resultierende Performanzprobleme im späteren Produktivbetrieb frühzeitig erkennen und vermeiden.

Außerdem unterstützt JPQL nur einen geringen Teil des SQL-Standards und keine datenbankspezifischen Abfragefeatures. Gerade für komplexere Abfragen ist JPQL daher nicht gut geeignet. In diesen Fällen sollten die später betrachteten nativen SQL-Abfragen bevorzugt werden.