Suche
EnterpriseTales

Wie frag’ ich die Datenbank? Typsichere dynamische Abfragen mit JPA

Arne Limburg, Lars Röwekamp

© Software & Support Media

Die Java Persistence Query Language (JPQL) ist sehr gut geeignet, statische Abfragen, die schon zur Entwicklungszeit feststehen, zu formulieren, sodass der Persistenz-Provider daraus SQL-Abfragen generieren kann. Wie sieht es aber in Situationen aus, in denen sich die Abfrage in Abhängigkeit von Laufzeitbedingungen ändert? Welche Möglichkeiten gibt es, dynamische JPA-Abfragen zu formulieren und dennoch bereits in der IDE und zur Compile-Zeit sicher zu sein, keinen Syntaxfehler eingebaut zu haben? Jeder kennt wohl den Use Case: Es gibt eine Suchmaske mit mehreren Feldern, z. B. „Vorname“ und „Nachname“. Gesucht werden soll die zugehörige Person. Eine Abfrage gegen die Datenbank muss man in Abhängigkeit davon absetzen, ob eines der Felder befüllt ist oder es beide sind. Wie kann man einen solchen Use Case mit JPA umsetzen, ohne auf Typsicherheit zu verzichten?

Für statische Abfragen ist in der heutigen Toolwelt eine recht gute Unterstützung zur Vermeidung von Tipp- und/oder Semantikfehlern vorhanden. Die meisten IDEs können mittlerweile JPQL-Abfragen bereits zur Entwicklungszeit analysieren und dann nicht nur direkt auf Syntaxfehler hinweisen, sondern finden sogar Unstimmigkeiten mit dem verwendeten JPA-Modell (falsche Entity-Namen, falsch verwendete Attribute etc.).

Auch wenn einige IDEs noch Schwierigkeiten haben, einen JPQL-String zu erkennen, wenn er direkt an entityManager.createQuery(…) übergeben wird, gelingt es mit den entsprechenden Plug-ins mittlerweile jeder IDE @NamedQueries(…) zu parsen und dort Fehler zu erkennen. In Eclipse z. B. muss man dazu nur das JPA-Facet aktivieren. Schon alleine deshalb lohnt es sich, Named Querys den Vorzug vor Querys zu geben, die man direkt über entityManager.createQuery(…) ausführt.

Vor diesem Hintergrund könnte man auf die Idee kommen, auch obigen Use Case mit Named Querys abzubilden. Man bräuchte ja nur drei Stück: eine, die nur nach dem Vornamen sucht, eine, die nur nach dem Nachnamen sucht, und eine, die nach Vor- und Nachnamen sucht.

Wenn es sich bei den optionalen Parametern nur um Vor- und Nachnamen handelt, mag das ja noch funktionieren. Man stößt mit diesem Vorgehen aber recht schnell an Grenzen. Spätestens ab dem dritten optionalen Parameter erhöht sich die Anzahl der benötigten Abfragen auf sieben, und man benötigt eine andere Technik, um die Abfrage zu definieren.

Auch der Ansatz, eine solche Anforderung über String-Konkatenation zu lösen, ist auf Dauer zum Scheitern verurteilt, weil die Wartbarkeit enorm leidet. Ganz zu schweigen davon, dass man damit in der Regel alle oben genannten Syntax- und Semantikchecks der IDE sofort aushebelt. Wie also soll dann mit einer solchen Situation umgegangen werden?

[ header = Seite 2: Das Criteria API ]

Das Criteria API

Mit der Version 2.0 von JPA ist die Antwort des Standards auf diese Frage das Criteria API [1]. Mit ihm ist es tatsächlich möglich, Query-Konstrukte dynamisch aufzubauen, ohne dass String-Konkatenation verwendet werden muss (Listing 1). Ab JPA 2.1 wird man auch Update- und Delete-Statements mit dem Criteria API formulieren können.

Etwas merkwürdig mutet beim Criteria API allerdings der Zugriff auf Attribute eines Objekts an: person.get(Person_.firstName). Die Klasse Person_ (nein, der Unterstrich ist kein Tippfehler) ist dabei eine automatisch generierte Klasse des ebenfalls mit JPA 2.0 eingeführten Metamodells, die typsicheren Zugriff auf eben diese Attribute bieten soll. Der leichter zu lesende Zugriff über die Angabe des Attributnamens als String, also person.get(„firstName“) ist zwar auch möglich und auch deutlich lesbarer, schützt aber nicht vor Tippfehlern.

Der etwas komplizierte Zugriff auf Attribute einer abzufragenden Entität über das Metamodell ist auch der erste Grund, warum Criteria-Querys recht schnell unübersichtlich werden. Während man in JPQL für den Zugriff auf das Attribut firstName der Entität Person einfach person.firstName schreiben kann, muss man im Criteria API besagten Umweg über die Metadatenklasse, nämlich person.get(Person_.firstName) verwenden. Dies erscheint bei einem so einfachen Beispiel zwar nur unwesentlich länger. Wenn man aber, wie es normalerweise der Fall ist, viele solcher Konstrukte in einer Abfrage verwenden muss, wächst der Code schnell an und wird damit schlechter wartbar. Der zweite Grund, warum das Criteria API schwer zu lesen ist, ist die Entscheidung des JPA Specification Committees, für das Erstellen von Query-Konstrukten den CriteriaBuilder einzuführen. Er erfordert die Verwendung einer funktionalen Syntax, bei der das Prädikat vorangestellt ist. Dies entspricht weder dem deutschen noch dem englischen Satzbau, weshalb sich solche Query-Konstrukte deutlich schlechter lesen lassen als ihr JPQL-Pendant. Schreibt man in JPQL z. B. person.firstName = :firstName, so lässt sich das leichter lesen als die Criteria-Variante: cb.equal(person.get(Person_.firstName), firstName). Auch hier gilt wieder, dass man in einer Criteria-Abfrage in der Regel nicht nur ein solches Konstrukt vorfindet, sondern gleich eine ganze Reihe, was eine Criteria-Query in der Summe deutlich unleserlicher macht als das zugehörige JPQL.

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Person> personQuery = cb.createQuery(Person.class);
Root<Person> person = personQuery.from(Person.class);
List<Predicate> condition = new ArrayList<Predicate>();
if (firstName != null) {
  condition.add(cb.equal(person.get(Person_.firstName), firstName));
}
if (lastName != null) {
  condition.add(cb.equal(person.get(Person_.lastName), lastName));
}
return entityManager.createQuery(
    personQuery
      .select(person)
      .where(condition.toArray(new Predicate[0])))
  .getResultList();

[ header = Seite 3: Querydsl ]

Querydsl

Genau diese Kritikpunkte geht das Open-Source-Framework Querydsl [1] an. Wie das Criteria API basiert es auf der Generierung eines Metadatenmodells. Im Gegensatz zur JPA-Standardvariante sind die entstehenden Abfragen durch ein Fluent-API aber deutlich leserlicher. Der bereits oben erwähnte Vergleich des Vornamens, der im Criteria API recht kompliziert aussah, nämlich cb.equal(person.get(Person_.firstName), firstName), lässt sich mit Querydsl deutlich leserlicher formulieren: person.firstName.eq(firstName). Allein dieser kleine API-Unterschied zwischen dem Criteria API und Querydsl macht dynamische Abfragen mit Querydsl deutlich leserlicher und damit wartbarer, als das im JPA-Standard möglich ist.

Einen kleinen Nachteil hat Querydsl dabei: Das implizite Joinen über Many-to-One-Beziehungen, wie es in JPQL z. B. mit person.address.street und mit dem Criteria API (etwas unleserlicher) mit person.get(Person_.address).get(Address_.street) möglich ist, geht in Querydsl nicht. Hier muss ein Join immer explizit formuliert werden (Listing 3).

Über die Unterstützung von JPA hinaus bietet Querydsl noch Adapter zum Bauen von nativem SQL, eine Anbindung von JDO und von nativem Hibernate. Letzteres dürfte vor allem für diejenigen interessant sein, die über den JPA-Standard hinaus Hibernate-spezifische Features nutzen, die sie auch in den dynamischen Abfragen nicht missen wollen. Zu guter Letzt bietet Querydsl sogar ein Modul für Abfragen gegen die NoSQL-Datenbank MongoDB.

QPerson person = QPerson.person;
List<Predicate> condition = new ArrayList<Predicate>;
if (firstName != null) {
  condition.add(person.firstName.eq(firstName));
}
if (lastName != null) {
  condition.add(person.lastName.eq(lastName));
}
return new JPAQuery(entityManager)
  .from(person)
  .where(condition.toArray(new Predicate[0]))
  .list(person);
QPerson person = QPerson.person;
QAddress address = QAddress.address;
person.join(person.address, address);
return new JPAQuery(entityManager)
  .from(person)
  .leftJoin(person.address, address)
  .where(address.street.eq(street))
  .list(person);

[ header = Seite 4: Einsatz in Spring Data ]

Einsatz in Spring Data

Wie wir im vorherigen Abschnitt gesehen haben, versucht Querydsl mit der großen Auswahl an Zielplattformen einen Abfragemechanismus zur Verfügung zu stellen, der deutlich über JPA hinausgeht. Damit passt er hervorragend in den Datenzugriffsableger des Spring Frameworks: Spring Data. Auch dieses versucht, vom Java-EE-Standard zu abstrahieren und eine gemeinsame Zugriffsschicht für JPA, JDO, JDBC, MongoDB u. a. zu bieten. Daher ist es naheliegend, dass Querydsl ein First-Class-Citizen in Spring Data ist. Mit Spring Data lassen sich Abfragen in Querydsl direkt absetzen, ohne Rücksicht auf die darunterliegende Persistenztechnologie. Das eigene Spring-Data-Repository muss nur das Interface org.springframework.data.querydsl.QueryDslPredicateExecutor implementieren, und schon kann man Querydsl-Prädikate direkt an das Repository übergeben, das daraus JPA-Abfragen erzeugt.

Fazit

Für dynamische Datenbankabfragen bietet JPA das Criteria API, das allerdings vor allem für komplexe Abfragen schnell recht unübersichtlich wird. Mit Querydsl haben wir hier eine interessante Alternative vorgestellt, mit der es möglich ist, deutlich lesbare dynamische Abfragen zu definieren.

Wer seine Datenzugriffsschicht bereits von Spring Data generieren lässt, muss nur ein weiteres Interface definieren, um mit dem Repository Querydsl-Abfragen absetzen zu können. Aber auch für alle anderen, die in ihren Projekten komplexe, dynamische JPA-Abfragen benötigen, lohnt sich ein Blick in Querydsl, um die Wartbarkeit der dynamischen Abfragen im Vergleich zum Criteria API deutlich zu erhöhen.

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. rwoo2013-12-12 20:11:01

    In Listing 2 sollte der BooleanBuilder statt der Liste verwendet werden. Der Code, insbesondere die where-Klausel wird dadurch einfacher lesbar.

  2. rwoo2013-12-12 20:11:01

    In Listing 2 sollte der BooleanBuilder statt der Liste verwendet werden. Der Code, insbesondere die where-Klausel wird dadurch einfacher lesbar.

Schreibe einen Kommentar

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