21.01.2012 von Tobias Trelle
Keine Kommentare
Was bisher geschah
Teil 1: Spring Data Commons
Teil 2: Spring Data JPA
Nachdem wir im ersten Teil dieser Blog-Serie das Spring Data Commons Projekt vorgestellt haben, wollen wir nun das Unterprojekt Spring Data JPA genauer unter die Lupe nehmen.
JPA
JPA ist als Teil des JEE-Stacks eine standardisierte Schnittstelle, mit der POJOs in relationalen Datenbanksystemen persistiert werden können. Mit einer eigenen Abfragesprache JPQL können Datenbankabfragen unabhängig von einem konkreten SQL-Dialekt formuliert werden. Für das Verständnis von Spring Data JPA sollten daher zumindest Grundkenntnisse des JPA APIs vorhanden sein.
Spring Data JPA
Aufbauend auf dem klassischen JPA Support bietet Spring Data JPA (u.a.) ein Konzept, mit dem JPQL-Queries wesentlich einfacher implementiert werden können. Üblicherweise sind Queries parametrisiert. Vor der Ausführung einer Query schreibt der Entwickler meistens eine Menge Boilerplate-Code, um diese Parameter zu setzen. Das wird dann klassich in einem Spring-Repository in etwa so implementiert:
@Entity
@NamedQuery( name="myQuery", query = "SELECT u FROM User u where u.fullName = :fullName" )
public class User {
...
}
@Repository
public class ClassicUserRepository {
@PersistenceContext EntityManager em;
public List<User> findByFullName(String fullName) {
TypedQuery<User> q = getEntityManger().createNamedQuery("myQuery", User.class);
q.setParameter("fullName", fullName);
return q.getResultList();
}
... |
@Entity @NamedQuery( name="myQuery", query = "SELECT u FROM User u where u.fullName = :fullName" ) public class User { ... } @Repository public class ClassicUserRepository { @PersistenceContext EntityManager em; public List<User> findByFullName(String fullName) { TypedQuery<User> q = getEntityManger().createNamedQuery("myQuery", User.class); q.setParameter("fullName", fullName); return q.getResultList(); } ...
Dieser Code lässt sich durch Nutzung des Fluent Intefaces von TypedQuery
zwar noch etwas kompakter formulieren …
@Repository
public class ClassicUserRepository {
@PersistenceContext EntityManager em;
public List<User> findByFullName(String fullName) {
return getEntityManger().createNamedQuery("myQuery", User.class)
.setParameter("fullName", fullName)
.getResultList();
}
... |
@Repository public class ClassicUserRepository { @PersistenceContext EntityManager em; public List<User> findByFullName(String fullName) { return getEntityManger().createNamedQuery("myQuery", User.class) .setParameter("fullName", fullName) .getResultList(); } ...
… dennoch schreibt man für jede Datenbankabfrage immer wieder eine Methode, die eine Liste von Parameter entgegennimmt, diese in die Query einsetzt und schließlich die Query ausführt. Mit Spring Data JPA lässt sich das wesentlich eleganter formulieren:
package repositories;
public interface UserRepository extends JpaRepository<User, String> {
List<User> findByFullName(String fullName);
} |
package repositories; public interface UserRepository extends JpaRepository<User, String> { List<User> findByFullName(String fullName); }
Die Grundidee ist die, dass man bereits aus der Signatur einer Interface(!)-Methode den bzw. die Namen des/der Query-Parameter ableiten kann. Spring liefert zur Laufzeit eine entsprechende Implementierung, die die entsprechende Query über das JPA Criteria API aufbaut und auch ausführt. Damit ist ein Großteil von Queries in der Praxis sehr schnell formuliert. Dieser Mechanismus läßt sich mit den allgemeinen Sortierungs- und Paginations-Aspekten kombinieren:
public interface UserRepository extends JpaRepository<User, String> {
List<User> findByFullName(String fullName, Sort sort);
List<User> findByFullName(String fullName, Pageable paging);
} |
public interface UserRepository extends JpaRepository<User, String> { List<User> findByFullName(String fullName, Sort sort); List<User> findByFullName(String fullName, Pageable paging); }
Ein weiterer Unterschied zur klassischen JPA-Entwicklung stellt die Möglichkeit dar, JPQL-Queries als Annotation an die Query-Methode zu schreiben. Üblicherweise werden die Queries mittels der @NamedQuery
an die JPA-Entity annotiert und müssen über einen symbolischen Namen aufgefunden werden. Dies ist mit Spring Data JPA nicht notwendig, da die Query direkt an die Repository-Methode annotiert wird:
@Transactional(timeout = 2, propagation = Propagation.REQUIRED)
@Query("SELECT u FROM User u WHERE u.fullName = 'User 3'")
List<User> findByGivenQuery(); |
@Transactional(timeout = 2, propagation = Propagation.REQUIRED) @Query("SELECT u FROM User u WHERE u.fullName = 'User 3'") List<User> findByGivenQuery();
Ich persönlich finde diesen Ansatz sehr gelungen, da auf diese Weise die Query dort formuliert wird, wo sie auch verwendet wird und nicht die JPA-Entität zumüllt.
Eine weitere nette Eigenschaft ist die, dass die Queries bereits beim Aufbau des Application Context validiert werden. So werden Syntax-Fehler in JPQL-Queries schnell gefunden. Dies geschieht bei Verwendung des reinen JPA APis sonst erst zur Laufzeit.
Beispiel
Den kompletten Source-Code findet man als Maven-Projekt auf Github. Zur Ausführung der Beispiele verwenden wir OpenJPA als JPA-Provider und HyperSQL DB als RDBMS. Der Unit-Test jpa.JpaRepoTest
ist ein guter Startpunkt.
Wesentlich ist, dass man konfiguriert, aus welcher Paketstruktur die Repository-Interfaces automagisch JPA-fiziert werden sollen:
<jpa:repositories base-package="jpa"/> |
<jpa:repositories base-package="jpa"/>
Bei mehr als einer verwendeten EntityManagerFactory
kann man angeben, welche konkrete Factory die Repositories verwenden sollen.
Was gibt’s sonst noch?
Wie im ersten Teil der Blog-Serie angekündigt, habe ich nur einige Aspekte von Spring Data JPA vorgestellt. Eine vollständige Feature-Liste findet man auf der Homepage des Projekts.
Ausblick
Die Projekte Spring Data MongoDB oder Spring Data Neo4J sind gute Kandidaten für weitere Beiträge in dieser Serie.