Spring Data avec JPA¶
Spring Data est un projet Spring qui a pour objectif de simplifier l’interaction avec différents systèmes de stockage de données : qu’il s’agisse d’une base de données relationnelle, d’une base de données NoSQL, d’un système Big Data ou encore d’une API Web.
Le principe de Spring Data est de simplifier le travail des développeurs en prenant en charge l’implémentation des méthodes d’accès à ces systèmes. Pour cela, Spring Data fournit des interfaces par défaut mais définit aussi une convention de nommage des méthodes d’accès pour nous permettre d’exprimer la requête à réaliser.
Notion de repository¶
Spring Data s’organise autour de la notion de repository. Il fournit
une interface marqueur générique Repository<T, ID>. Le type T
correspond
au type de l’objet géré par le repository. Le type ID
correspond au type
de la clé d’un objet.
Note
La notion de repository est empruntée à l’ouvrage de Eric Evans : Domain-Driven Design (Addison-Wesley 2003). Un repository est une abstraction nous permettant de manipuler les objets du domaine métier. Le terme Repository se veut neutre par rapport à l’implémentation sous-jacente.
L’interface CrudRepository<T, ID> hérite de Repository<T, ID> et fournit un ensemble d’opérations élémentaires pour la manipulation des objets.
Spring Data JPA¶
Le projet Spring Data est en fait un regroupement de modules fournissant chacun la possibilité de configurer et d’injecter une implémentation d’un repository pour un type de systèmes de données.
Spring Data JPA est le module qui nous permet d’interagir avec une base de données relationnelles en représentant les objets du domaine métier sous la forme d’entités JPA.
Spring Data JPA fournit l’interface JpaRepository<T, ID> qui hérite de CrudRepository<T, ID> et qui fournit un ensemble de méthodes plus spécifiquement adaptées pour interagir avec une base de données relationnelle.
Pour définir un repository, il suffit de créer une interface qui hérite d’une des interfaces ci-dessus. Nous allons voir que Spring Data JPA va prendre en charge, à l’exécution, la création d’une classe implémentant cette interface.
package dev.gayerie.repositories;
import dev.gayerie.service.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
Note
Notez que l’interface ne porte aucune annotation. UserRepository
ne peut
pas servir à déclarer un composant Spring puisqu’il s’agit simplement d’une
interface.
Intégration des repositories¶
Selon que vous utilisiez ou non Spring Boot, l’intégration du support des repositories dans votre application va se faire de manière légèrement différente.
Intégration dans une application Spring Boot¶
Spring Boot est un projet conçu pour simplifier considérablement la configuration des applications basées sur le Spring Framework. Spring Data JPA est automatiquement inclus et configuré si vous ajoutez le support à JPA. Consultez le chapitre Spring DAO pour voir comment faire.
Par défaut, Spring Data JPA va utiliser le package de la classe portant l’annotation @SpringBootApplication comme le package de base. Cela signifie, qu’il va rechercher les interfaces de repositories dans ce package et tous ses sous-packages.
Si vous voulez changer le comportement par défaut, vous pouvez utiliser l’annotation @EnableJpaRepositories décrite pour l’intégration dans une application sans Spring Boot.
Intégration dans une application sans Spring Boot¶
Pour intégrer Spring Data JPA dans une application sans Spring Boot, vous devez d’abord intégrer le support de JPA et des transactions. Reportez vous au chapitre Spring Transaction pour voir comment faire.
Ensuite, il vous faut ajouter la dépendance à Spring Data JPA :
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>2.4.1</version>
</dependency>
Enfin, vous devez activer le support des repositories JPA en ajoutant l’annotation @EnableJpaRepositories sur un bean de configuration.
package dev.gayerie;
import javax.persistence.EntityManagerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalEntityManagerFactoryBean;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableJpaRepositories
@EnableTransactionManagement
public class MyApplication {
@Bean
public LocalEntityManagerFactoryBean entityManagerFactory() {
LocalEntityManagerFactoryBean factoryBean = new LocalEntityManagerFactoryBean();
factoryBean.setPersistenceUnitName("database");
return factoryBean;
}
@Bean
public TransactionManager transactionManager(EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
public static void main(String[] args) {
try (AnnotationConfigApplicationContext appCtx =
new AnnotationConfigApplicationContext(MyApplication.class)) {
// ...
}
}
}
L’annotation @EnableJpaRepositories fournit les attributs suivants :
- basePackages
Indique les packages de base (incluant leurs sous-packages) à partir desquels Spring Data JPA recherche des interfaces de repositories. Par défaut, la recherche se fera à partir du package de la classe qui porte l’annotation @EnableJpaRepositories.
Astuce
Si vous avez dans votre projet une interface héritant de Repository<T, ID> mais que vous ne souhaitez pas que Spring Data génère de classe concrète, alors vous devez ajouter l’annotation @NoRepositoryBean sur cette interface.
- basePackagesClasses
Fonctionne comme
basePackages
sauf que l’on fournit le tableau des classes. La recherche des interfaces se fera à partir du package de chacune des classes (en incluant leurs sous-packages).- enableDefaultTransactions
Signale si une méthode de repository est transactionnelle par défaut. Attention, cet attribut a la valeur
true
par défaut. Si votre projet gère les transactions avec Spring Transaction en utilisant des classes de service qui délèguent des appels aux repositories, alors il est plus cohérent de positionner cet attribut àfalse
. Reportez-vous au chapitre Spring Transaction pour en savoir plus.- entityManagerFactoryRef
Donne le nom du bean de type EntityManagerFactory à utiliser. Par défaut, Spring Data JPA recherche dans le contexte un bean nommé
entityManagerFactory
.- transactionManagerRef
Donne le nom du bean de type JpaTransactionManager à utiliser. Par défaut, Spring Data JPA recherche dans le contexte un bean nommé
transactionManager
.
Note
Pour la configuration des beans entityManagerFactory
et transactionManager
,
reportez-vous au chapitre Spring Transaction.
Injection des repositories¶
À l’initialisation du contexte d’application, Spring Data JPA va rechercher à partir du ou des packages de base (en incluant leurs sous-packages) toutes les interfaces de répositories, c’est-à-dire toutes les interfaces héritant directement ou indirectement de l’interface Repository<T, ID>. Pour chacune de ces interfaces, Spring Data JPA va fournir une classe d’implémentation et créer un bean portant le même nom que l’interface.
Pour utiliser une interface repository dans une application, il suffit d’injecter un bean du type de l’interface.
package dev.gayerie.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import dev.gayerie.repository.UserRepository;
@Repository
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void doSomething(long id) {
long nbUser = userRepository.count();
boolean exists = userRepository.existsById(id);
// ..
}
}
Ajout de méthodes dans une interface de repository¶
L’interface JpaRepository<T, ID> déclare beaucoup de méthodes mais elles suffisent rarement pour implémenter les fonctionnalités attendues d’une application. Spring Data JPA utilise une convention de nommage pour générer automatiquement le code sous-jacent et exécuter la requête. La requête est déduite de la signature de la méthode (on parle de query methods).
La convention est la suivante : Spring Data JPA supprime du début de la méthode
les prefixes find
, findAll
, read
, query
, count
et get
et
recherche la présence du mot By
pour marquer le début des critères de filtre.
Le terme après By
fait référence à un attribut de l’entité JPA pour lequel
on veut appliquer un filtre. Chaque critère doit correspondre à un paramètre de
la méthode en respectant l’ordre.
Note
Pour une description complète des règles de nommage existantes pour les query methods, vous pouvez vous reporter à la documentation officielle.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | package dev.gayerie.repositories;
import dev.gayerie.service.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
User getByLogin(String login);
long countByEmail(String email);
List<User> findByNameAndEmail(String name, String email);
List<User> findByNameOrEmail(String name, String email);
}
|
Spring Data JPA générera une implémentation pour chaque méthode de ce repository.
Par exemple, pour la méthode getByLogin, l’implémentation sera de la forme :
return entityManager.createQuery("select u from User u where u.login = :login", User.class)
.setParameter("login", login)
.getSingleResult();
Pour la méthode countByEmail, l’implémentation sera de la forme :
return (Long) entityManager.createQuery("select count(u) from User u where u.email = :email")
.setParameter("email", email)
.getSingleResult();
Pour la méthode findByNameAndEmail, l’implémentation sera de la forme :
return entityManager.createQuery("select u from User u where u.name = :name and u.email = :email", User.class)
.setParameter("name", name)
.setParameter("email", email)
.getResultList();
Pour la méthode findByNameOrEmail, l’implémentation sera de la forme :
return entityManager.createQuery("select u from User u where u.name = :name or u.email = :email", User.class)
.setParameter("name", name)
.setParameter("email", email)
.getResultList();
Note
Il est même possible de donner des critères sur des entités liées. Ainsi,
si la classe User
contient une association vers une entité Address
:
package dev.gayerie.model;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
@Entity
public class User {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
@OneToOne
private Address adress;
// ...
}
et si l’entité Address
contient un champ city
:
package dev.gayerie.model;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Address {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String city;
// ...
}
Alors il est possible de définir une méthode dans UserRepository
qui permet
de filtrer sur la ville de l’adresse :
List<User> findByAddressCity(String city);
Requêtes nommées JPA¶
Avec JPA, il est possible de définir des requêtes nommées grâce à l’annotation @NamedQuery.
Spring Data JPA utilise une convention pour rechercher les requêtes nommées
avec JPA. La requête doit porter comme nom, le nom de l’entité suivi de .
suivi du nom de la méthode. Ainsi si on définit une requête nommée sur une
entité User
:
package dev.gayerie.model;
import dev.gayerie.service.User;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQuery;
@Entity
@NamedQuery(name="User.findByLogin", query="select u from User u where u.login = :login")
public class User {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String login;
// ...
}
Il faut ensuite déclarer la méthode dans le repository assigné à l’entité User
:
package dev.gayerie.repositories;
import dev.gayerie.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
public interface UserRepository extends JpaRepository<User, Long>{
User findByLogin(@Param("login") String login);
}
Note
Remarquez la présence de l’annotation @Param qui permet d’associer le paramètre de la méthode au paramètre de la requête nommée.
Utilisation de @Query¶
L’annotation @Query permet de préciser la requête directement sur la méthode elle-même :
package dev.gayerie.repositories;
import dev.gayerie.service.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface UserRepository extends JpaRepository<User, Long>{
@Query("select u from User u where u.login = :login")
User findByLogin(@Param("login") String login);
}
Le comportement par défaut de Spring Data JPA est de chercher la présence de l’annotation @Query puis la présence d’une requête nommée JPA. S’il n’en existe pas alors Spring Data JPA analyse la signature de la méthode pour essayer d’en déduire la requête à exécuter.
Note
Pour des requêtes avec peu de paramètres, il est possible d’utiliser la notation pour désigner un paramètre par un numéro d’ordre dans la requête. Cela évite un usage de l’annotation @Param :
package dev.gayerie.repositories;
import dev.gayerie.service.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
public interface UserRepository extends JpaRepository<User, Long>{
@Query("select u from User u where u.login = ?1")
User findByLogin(String login);
}
Déclaration de requêtes de modification¶
Il est possible de créer des query methods pour réaliser des modifications (update, insert, delete). Pour cela, il suffit d’ajouter l’annotation @Modifying sur la méthode :
package dev.gayerie.repositories;
import dev.gayerie.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
public interface UserRepository extends JpaRepository<User, Long>{
@Modifying
@Query("update User u set u.login = ?2 where u.id = ?1")
void updateLogin(long id, String login);
}
L’annotation @Modifying s’utilise toujours conjointement avec l’annotation @Query ou une requête nommée JPA car il n’existe pas de convention de nommage pour des requêtes de modification.
Implémentation des méthodes de repository¶
Il est parfois nécessaire de fournir une implémentation d’une ou de plusieurs
méthodes d’un repository. Dans ce cas, il faut isoler les méthodes que l’on
souhaite implémenter dans une interface spécifique. Par exemple, on peut
créer l’interface UserCustomRepository
:
package dev.gayerie.repositories;
import dev.gayerie.model.User;
public interface UserCustomRepository {
void doSomethingComplicatedWith(User u);
}
Cette interface est étendue par l’interface du repository :
package dev.gayerie.repositories;
import dev.gayerie.service.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends UserCustomRepository, JpaRepository<User, Long>{
}
Comme Spring Data JPA détecte une interface parente qui n’hérite pas elle-même
de l’interface Repository<T, ID>, il recherche une classe Java portant le même
nom que l’interface avec le suffixe Impl
dans le même package ou un
sous-package. Si une telle classe existe alors Spring Data JPA tente de
créer un bean de cette classe.
Note
La classe d’implémentation ne doit pas porter de stéréotype Spring comme @Component ou @Repository. Par contre, elle peut utiliser toutes les autres annotations autorisées par le Spring Framework pour profiter notamment de l’injection de dépendances.
Astuce
Si le suffixe Impl
ne vous convient pas, vous pouvez changer la valeur
du suffixe grâce à l’annotation @EnableJpaRepositories et à son attribut
repositoryImplementationPostfix
.
package dev.gayerie.repositories;
import dev.gayerie.service.User;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
public class UserCustomRepositoryImpl implements UserCustomRepository {
@PersistenceContext
private EntityManager em;
@Override
public void doSomethingComplicatedWith(User u) {
// ...
}
}
Le repository fonctionnera ainsi par délégation. Lorsque la méthode
UserRepository.doSomethingComplicatedWith
sera appelée, elle déléguera le
traitement à la méthode UserCustomRepositoryImpl.doSomethingComplicatedWith
.
Note
Il est tout à fait possible de fournir une implémentation pour une méthode déclarée dans l’interface JpaRepository<T, ID> ou une des interfaces parentes. Pour cela, il suffit de déclarer dans l’interface d’implémentation une méthode avec la même signature.