Spring Data, JPA 2 und Querydsl

Spring Data, JPA 2 und Querydsl

Spring Data, JPA 2 und Querydsl

Spring Data, JPA 2 und Querydsl

Spring Data, JPA 2 und Querydsl


Die Kombination von Querydsl und Spring Data ist derzeit konkurrenzlos. Kein anderes Framework schafft es, die Menge an Boilerplate-Code so konsequent zu reduzieren und dabei ein so elegantes API bereitzustellen. Getreu dem Prinzip: „Die beste Zeile Code ist die, die man erst gar nicht schreibt“ enthalten Repositories am Ende nur das, was sie auch wirklich brauchen. Für den Entwickler bedeutet das: mehr Spaß bei der Arbeit mit relationalen Datenbanken.

Ziel dieses Artikels ist, zu zeigen, wie die Verwendung von Spring Data in Verbindung mit JPA 2 und Querydsl das Leben extrem erleichtern kann. Vorausgesetzt wird, dass man weiß, wie man einen ORM (Object Relational Mapper) aufsetzt und die Konzepte hinter JPA 2 (Annotationen, EntityManager usw.) beherrscht. Den vollständigen Code zu diesem Artikel samt der zugehörigen ORM-Konfiguration findet man auf GitHub [1].

ORMs sind nicht aus der Softwareentwicklung wegzudenken. In Sachen Performance und Verwendung von speziellen DB-Features hat sich hier einiges getan. Auf der Strecke blieben dabei notwendige Weiterentwicklungen des API. Es mag ein subjektiver Eindruck sein, aber seit einer gefühlten Ewigkeit waren wir gezwungen, Mapping-Informationen in XML-Dateien abzulegen und Querys mühsam per Hand zu schreiben und zu pflegen.

Typsicherheit war hier schon immer ein Fremdwort, und die Unmengen an Boilerplate-Code waren nur schwer zu verstecken. Ständig hat man aus älteren Projekten DAO-Implementierungen kopiert oder sich zum x-ten Mal über einen Tippfehler in der Query geärgert.

Zugegeben, die Sache mit den XML-Mapping-Informationen sind wir mittlerweile losgeworden, auch wenn es immer noch genügend Leute gibt, die sich dagegen wehren. Die fehlende Typsicherheit ist das größte Problem. Egal, wie intelligent eine IDE mit der Erzeugung einer Query umgeht, er bleibt ein String, und Probleme werden im schlimmsten Fall erst erkannt, wenn es schon zu spät ist. Wer jetzt das Criteria-API anführen möchte, sei auf später vertröstet.

Spring Data

Bei Spring Data handelt es sich um ein „Umbrella“-Projekt ähnlich Spring Security oder Spring Social. Ziel war es, die Verwendung verschiedenster Data Stores so einfach wie möglich zu gestalten, ohne dabei Funktionalitäten zu beschränken. Dabei legte man besonderen Wert darauf, die Unterschiede der Stores nicht zu verwischen. Schließlich möchte man keine relationale Algebra auf Neo4j abbilden, sondern mit Graphen arbeiten.

Das so entstandene Projekt bietet Anbindungen für verschiedenste dokumentenorientierte Datenbanken, Key-Value Stores und andere alternative Konzepte. Daneben hat man auch daran gedacht, sich um den immer noch dominantesten Anteil unter den Data Stores zu kümmern: die relationale Datenbank.

Spring Data JPA

ORM ist für viele Datenbankanwendungen das Mittel der Wahl, wenn es darum geht, von Java aus mit einer DB zu interagieren. Über den richtigen ORM lässt sich vortrefflich streiten. Glücklicherweise will sich Spring Data in diese Diskussion nicht einmischen. Man setzte auf die Verwendung von JPA 2 als Basis. Durch diesen Standard war es möglich, fast vollkommen unabhängig von einer konkreten Implementierung zu bleiben. JPA  2 ist aber bei Weitem nicht perfekt, und man ist immer noch an einigen Ecken dazu gezwungen, Vendor-Erweiterungen zu verwenden. Für die Aufgaben von Spring Data benötigt man aber keine davon.

Bootstrap

Details zur Konfiguration von Hibernate findet man, wie eingangs erwähnt, im Beispielprojekt. Um nun Spring Data verwenden zu können, muss man die entsprechende Abhängigkeit im Build eintragen:

 org.springframework.data:spring-data-jpa:1.3.1.RELEASE

Nachdem diese angezogen wurde, muss noch der Spring-Applikationskontext erweitert werden. Hier hat man sich sehr stark am Context Component Scan-Mechanismus orientiert. Spring Data benötigt nur die Information, in welchen Packages es nach Repositories zu suchen hat. Folgendermaßen wird dies mit Java Config erreicht:

...
@EnableJpaRepositories("com.senacor.repository")
public class MyConfiguration {

Für diejenigen, die kein Java Config einsetzen, zeigt Listing 1 das äquivalente Beispiel in XML.

Listing 1

...
<beans 
...
       xmlns:jpa="http://www.springframework.org/schema/data/jpa" 
       xsi:schemaLocation="
...
      http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
...>
  <jpa:repositories base-package=" com.senacor.repository " />
</beans>

In beiden Fällen weise ich Spring Data an, im Package com.senacor.repository nach Repository-Definitionen zu suchen. Jetzt wird es Zeit, das erste Repository zu definieren.

Repositories

Repositories sind der Dreh- und Angelpunkt aller Spring-Data-Projekte. Sie sind die Abstraktion zum Zugriff auf den jeweiligen Data Store. Im Fall von JPA 2 kapselt ein Repository den EntityManager und stellt verschiedene Kombinationen von Methoden zum Umgang mit selbigem bereit. Am besten lässt sich das mit einem Beispiel erklären. Anhand der UserEntity aus Listing 2 werde ich den Repository-Mechanismus näher erklären.

Listing 2

@Entity
public class UserEntity implements Serializable {
  @Id
  private Long id;
  private String firstname;
  private String lastname;
  @Temporal(TemporalType.DATE)
  private Date birthday;
 
  ...

Um ein CrudRepository (Create, Retrieve, Update, Delete) zu erzeugen, muss ich nur das entsprechende Interface (Listing 3) ableiten.

Listing 3

public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
  <S extends T> S save(S entity);
  <S extends T> Iterable<S> save(Iterable<S> entities);
  T findOne(ID id);
  boolean exists(ID id);
  Iterable<T> findAll();
  Iterable<T> findAll(Iterable<ID> ids);
  long count();
  void delete(ID id);
  void delete(T entity);
  void delete(Iterable<? extends T> entities);
  void deleteAll();
}

Das Listing wird dann in einem der von mir festgelegten Packages (s. o.) abgelegt:

public interface UserEntityRepository extends CrudRepository<UserEntity, Long>{}

Von jetzt an lässt sich mein UserEntityRepository durch Einsatz von @Autowired an beliebiger Stelle in der Anwendung verwenden:

@Autowired 
UserEntityRepository userEntityrepository;
...
Iterable<UserEntity> allEntities = userEntityrepository.findAll();
...

Das war’s auch schon. Von nun an sind so alle essenziellen CRUD-Operationen für die UserEntity verfügbar.

Wie geht das?

Eine Zeile Code (wenn man die Imports unterschlägt), um ein Repository zu erzeugen – da wird so mancher böse Magie vermuten. Entzaubern wir das Ganze: Spring Data setzt, genau wie Spring, sehr stark auf die Verwendung von Proxies. Für jedes Interface, das von org.springframework.data.repository.Repository ableitet, wird somit ein entsprechender Proxy erzeugt. Dessen Aufgabe ist es, Aufrufe am Interface an ihre Entsprechung am SimpleJpaRepository durchzuleiten. Das SimpleJpaRepository stellt alles an benötigten Methoden und Funktionalitäten bereit. Die Repository-Interfaces werden genutzt, um immer nur das bereitzustellen, was auch tatsächlich benötigt wird. Das Ganze wird mit einem Beispiel etwas klarer.

Read-only Repository

Wie wäre es z. B. mit einem ReadOnly-Repository für meine UserEntity? Dazu werfen wir einen Blick in die Definition des CrudRepository (Listing 3, eine schamlose Kopie aus den Spring-Quellen).

Mit diesem Wissen kann ich mein eigenes ROCrud­Repository (Listing 4) erzeugen. Wichtig ist hierbei die Verwendung von @NoRepositoryBean. Diese weist Spring Data das Interface beim Scan zu ignorieren, obwohl es von Repository ableitet.

Listing 4

@NoRepositoryBean
public interface RORepository<T, ID extends Serializable> extends Repository<T, ID> {
  T findOne(ID id);
  boolean exists(ID id);
  Iterable<T> findAll();
  Iterable<T> findAll(Iterable<ID> ids);
  long count();
}

Der Vollständigkeit halber ist hier die Verwendung des neuen Repository-Typen:

public interface ReadOnlyUserEntityRepository extends RORepository<UserEntity, Long>{}

Repositories sind also extrem flexibel an den jeweiligen Anwendungsfall anpassbar. Sie sind typsicher und bieten bis auf eine „Kleinigkeit“ alles, was man braucht.

Eigene Querys hinzufügen

Was noch fehlt, ist die Möglichkeit, eigene Abfragen zu formulieren. Hier bietet Spring Data mehrere Wege. Für die folgenden Beispiele soll eine Query erzeugt werden, die alle User mit einem bestimmten Nachnamen und einem teilweise angegebenen Vornamen liefert:

select user from User user where user.firstname like :firstname and user.lastname = :lastname

Die einfachste Variante, sie zu erzeugen, ist, sie gar nicht erst selbst zu schreiben, sondern die Arbeit Spring Data zu überlassen. Bereits erklärt wurde, wie die Methoden aus den Repository-Interfaces auf das SimpleJpaRepository abgebildet werden. Für alle Methoden, die keine Entsprechung an selbigem haben oder anderweitig implementiert wurden, erzeugt Spring Data eine Query. Der Mechanismus funktioniert dabei wie folgt:

  • Eine Methode, die mit findBy beginnt, wird automatisch als Kandidat erkannt.

  • Bei allen anderen Methoden überprüft man, ob sie mit dem Namen einer Property der Entität beginnen.

  • Die Parameter der Methode werden entsprechend der Reihenfolge als Queryparameter interpretiert.

Eine solche Methode sieht wie folgt aus:

public interface UserEntityRepository extends CrudRepository<UserEntity, Long>{
  List<User> findByFirstnameLikeAndLastnameEquals(String firstname, String lastname);
}

Alternativ hätte ich auch das findBy weglassen können, ich bevorzuge aber explizite Methodennamen. Ob man diese Variante insgesamt mag oder nicht, ist Geschmackssache. Aber es gibt noch mehr Möglichkeiten, mein Ziel zu erreichen.

Manchmal benötigt man nur einen Tick mehr Kontrolle über die erzeugte Query. Diese liefert Variante zwei. Hierbei werden die Query und ihre Parameter durch die Verwendung von @Query- und @Param-Annotationen erzeugt:

public interface UserEntityRepository extends CrudRepository<UserEntity,Long>{
  @Query("select user from UserEntity user where user.firstname like :firstname% and user.lastname = :lastname")
  List<UserEntity> findWhereFirstnameLikeAndLastnameEquals(@Param("firstname") String firstname, @Param("lastname") String lastname);
}

So können deutlich komplexere Querys formuliert werden, ohne dass man signifikant mehr Code benötigen wurde. Für die ganz harten Fälle gibt es dann noch eine dritte Variante. Bei dieser übernehme ich die Query­erzeugung komplett selbst. Allerdings muss ich hierfür die bisher vorgestellten Mechanismen umgehen. Dies gelingt durch die Auslagerung der entsprechenden Methoden in ein separates Interface:

public interface UserEntityRepositoryCustom {
  List<UserEntity> findByFirstnameLikeAndLastnameEquals(String firstname,String lastname);
}

Daraufhin wird eine Implementierung bereitgestellt (Listing 5).

Listing 5

public class UserEntityRepositoryImpl implements UserEntityRepositoy{
  @PersistenceContext
  EntityManager em;
 
  List<UserEntity> findByFirstnameLikeAndLastnameEquals(String firstname, String lastname) {
    Query q = em.createQuery("select user from UserEntity user where user.firstname like :firstname% and user.lastname = :lastname");
    q.setParameter("firstname", firstname);
    q.setParameter("lastname", lastname);
    return q.getResultList();
  }
}
public interface UserEntityRepository extends CrudRepository<UserEntity,Long>, UserEntityRepositoryCustom {}

Querydsl

Eine Sache stört allerdings an allen bisher vorgestellten Varianten: Sie sind weder Typ- noch Refactoring-sicher. Der informierte ORM-Entwickler wird jetzt natürlich den Finger erheben und anmerken, dass ich Variante drei durch die Verwendung des JPA-2-Criteria-API typsicher machen könnte. Die schreckgeweiteten Augen seiner Kollegen werden ihn dann schnell seinen Finger wieder sinken lassen. Wer schon mal mit diesem API-Verbrechen zu tun hatte, wird verstehen, weshalb ich sie für ORM meide wie Struts für Webanwendungen. So gut die Ideen auch sein mögen, die in ihr stecken, so sehr verdarben mir die Unmengen an Boilerplate und das akademische API-Design den Spaß an der Arbeit damit (dieser Satz sollte auf keinen Fall als Aufwertung von Struts gewertet werden). Glücklicherweise gibt es seit einiger Zeit eine großartige Alternative: Mit Querydsl wird es zum Kinderspiel, alle meine Anforderungen zu erfüllen.

Bootsrapping Querydsl

Zwei Abhängigkeiten sind notwendig, um Querydsl nutzen zu können:

com.mysema.querydsl:querydsl-jpa:2.9.0
com.mysema.querydsl:querydsl-apt:2.9.0

Die Erste ist das eigentliche API zum Umgang mit JPA 2 (es gibt noch eine ganze Reihe anderer Anbindungen, die allerdings nicht in den Rahmen dieses Artikels passen). Bei der Zweiten handelt es sich um den JPA Annotation Processor. Dieser wird benötigt, um die statischen Metainformationen zur Build-Zeit zu erzeugen. Wie, das sieht man in Listing 6 und 7. Sie zeigen die Verwendung mit Gradle, respektive Maven.

Listing 6

dependencies {
  compile libs.jpa20
  compile libs.querydsl_apt
  compile libs.commons_lang
}
 
def generatedDir = "$projectDir/src/main/generated"
 
sourceSets {
  generated.java.srcDirs = [generatedDir]
}
 
configurations {
  querydslapt
}
 
task generateQueryDSL(type: JavaCompile, group: 'build', description: 'Generates the QueryDSL query types') {
  source = sourceSets.main.java
  classpath = configurations.compile + configurations.querydslapt
  options.compilerArgs = [
            "-proc:only",
            "-processor", "com.mysema.query.apt.jpa.JPAAnnotationProcessor"
            ]
  destinationDir = new File(generatedDir)
  dependencyCacheDir = new File("$buildDir/dependencyCacheDir")
}

Listing 7

<plugin>
  <groupId>com.mysema.maven</groupId>
  <artifactId>maven-apt-plugin</artifactId>
  <executions>
    <execution>
      <goals>
        <goal>process</goal>
        <goal>test-process</goal>
      </goals>
      <configuration>
        <outputDirectory>target/generated-sources/java</outputDirectory>
        <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
      </configuration>
    </execution>
  </executions>
</plugin>

Bei den statischen Metainformationen handelt es sich um die so genannten Queryobjekte.

Nach dem Lauf des JPAAnnotationProcessor findet man für jede Entität eine weitere Klasse mit dem gleichen Namen und einem vorangestellten „Q“ (in meinem Beispiel wird zur existierenden UserEntity die QUserEntity erzeugt). Sie enthält statt der Entity Properties eine Pfadbeschreibung in Form von public static Members. So gibt es statt (get/set)Firstname eine firstname-Methode. Diese werden wiederum in Querys eingesetzt und liefern uns alles, was wir brauchen, um mithilfe des Builder Patterns eine Query aufzubauen (Listing 8).

Listing 8

public class UserEntityRepositoryImpl implements UserEntityRepositoy{
  @PersistenceContext
  EntityManager em;
 
  List<User> findWhereFirstnameLikeAndLastnameEquals(String firstname,String lastname) {
    JPAQuery query = new JPAQuery(entityManager);
    QUserEntity qUserEntity = QUserEntity.userEntity;
    return query.from(qUserEntity).where(qUserEntity.firstname.like(firstname).and(qUserEntity.lastname.eq(lastname))).list(qUserEntity);
  }
}

Diese Lösung ist typsicher, Refactoring-sicher und gut lesbar. Ganz zufrieden bin ich aber immer noch nicht. Auch für einfache Querys ist immer noch vergleichsweise viel Code nötig. Das geht noch besser.

QueryDslPredicateExecutor

Fangen wir mit dem UserEntityRepository noch mal von vorne an und fügen das Interface QueryDslPredicateExecutor hinzu:

public interface UserEntityRepository extends CrudRepository<UserEntity,Long>, QueryDslPredicateExecutor<UserEntity>{
}

Durch diese kleine Änderung bekommen wir einen ganzen Satz neuer Methoden:

public interface QueryDslPredicateExecutor<T> {
  T findOne(Predicate predicate);
  Iterable<T> findAll(Predicate predicate);
  Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orders);
  Page<T> findAll(Predicate predicate, Pageable pageable);
  long count(Predicate predicate);
}

Diese enthalten in ihrer Signatur das Predicate-Objekt, bei dem es sich um eine Repräsentation der where Clause handelt. Mithilfe der Q-Objekte ist der Aufbau der Prädikate sehr einfach, und wir können die folgende Beispielquery erzeugen:

@Autowired 
UserEntityRepository userEntityrepository;
...
QUserEntity qUserEntity = QUserEntity.userEntity;
userEntityrepository.findAll(qUserEntity.firstname.like(firstname).and(qUserEntity.lastname.eq(lastname)));
...

Und mit diesem Beispiel bin ich da angekommen, wo ich hin wollte. Querys lassen sich durch einfaches Betätigen der Auto-Complete-Funktion erstellen. Änderungen an den Entitäten führen zu Build-Fehlern, und ihre Auswirkungen werden frühzeitig erkannt. Der komplette Zugriff auf die Datenbank wurde gegenüber reinem JPA 2 mit dem Criteria-API deutlich entschlackt.

Ergebnis

Ich gebe es zu: Ich bin schon immer ein Spring-Fan und hatte noch nie ein Problem damit, statt des gesetzten den gelebten Standard zu verwenden. Nach der Enttäuschung, die JPA 1 darstellte, war ich dann in vielen Punkten doch angenehm von JPA 2 überrascht. Das änderte sich schnell, als ich das Criteria-API das erste Mal in einem Projekt einsetzen durfte.

Die zugrunde liegenden Ideen waren ja sehr gut – aber was nützen alle guten Ideen, wenn sie sich hinter einem derart hässlichen API verstecken? Der Wechsel zu Querydsl war die logische Konsequenz, und ich habe es bisher in keinem Projekt bereut. Im Gegenteil: Neue Entwickler kommen mit diesen Konzepten deutlich besser zurecht und finden einen schnelleren Einstieg in das Arbeiten mit ORMs.

Die Kombination von Querydsl und Spring Data ist in meinen Augen derzeit konkurrenzlos. Kein anderes Framework schafft es, die Menge an Boilerplate-Code so konsequent zu reduzieren und dabei ein so elegantes API bereitzustellen. Getreu dem Prinzip: „Die beste Zeile Code ist die, die man erst gar nicht schreibt“ enthalten Repositories am Ende nur das, was sie auch wirklich brauchen.

Wie bereits erwähnt, gibt es eine Vielzahl verschiedener Integrationen im Spring-Data-Projekt, und ich kann jedem Entwickler nur empfehlen, sich mit diesen zu beschäftigen. Ich selbst verwende derzeit die Integration von Neo4j und MongoDB und bin mehr als zufrieden. Die aktive Community beschert uns hier eine wachsende Anzahl von Optionen. Kompliment an die Teams von Spring Data und Querydsl!

mader_jochen_sw.tif_fmt1.pngJochen Mader ist Chief Developer bei der Senacor Technologies AG, wo er sich mit der Umsetzung komplexer Mehrschichtenanwendungen beschäftigt. Angetrieben durch seine Erfahrungen aus Code­reviews ist er immer auf der Suche nach Techniken, um Copy and Paste und Boilerplate-Code vollständig in Projekten zu eliminieren.

Twitter
Jochen Mader

Jochen Mader ist Chief Developer bei der Senacor Technologies AG, wo er sich mit der Umsetzung komplexer Mehrschichtenanwendungen beschäftigt. Angetrieben durch seine Erfahrungen aus Codereviews ist er immer auf der Suche nach Techniken, um Copy and Paste und Boilerplate-Code vollständig in Projekten zu eliminieren.