I. Notions▲
On se focalise sur les notions suivantes pour faire le choix de la méthode :
- simplicité à implémenter et réutilisabilité ;
- graphe ;
- projection ;
- statique ou dynamique ;
- problèmes JPA.
Ces notions (hormis la première qui est basique) sont présentées par la suite.
Tout ceci permet d'arriver à formuler le concept de requête efficace.
I-A. Graphe▲
En JPA la requête va manipuler une entité de départ (root) et éventuellement des entités en jointures.
Si seule l'entité root est à retourner alors il n'y a pas de graphe.
Attention seule la partie à retourner est à prendre en compte, si des jointures sont nécessaires uniquement pour la clause where cela n'entraîne pas un graphe.
I-B. Projection▲
En JPA il existe plusieurs types de projection :
- Scalar : retourne un résultat non typé ;
- DTO : retourne un résultat typé contenant certains champs d'une entité (ou de plusieurs si graphe) ;
- Entités : retourne un résultat de type entité.
L'utilisation de la projection scalar est à éviter, car on peut tout faire avec les autres et elles sont plus faciles d'utilisation.
I-B-1. Projection DTO▲
La projection DTO a pour principal avantage de ne pas avoir à récupérer tous les champs de la table. Elle nécessite de créer un Objet (le DTO) qui correspond au résultat de la requête. Elle est à utiliser dans le cas read only uniquement (difficile d'appeler un save sur l'entité par la suite). Elle est plus performante que la projection entités surtout dans le cas où le nombre de lignes retournées est très grand ou encore dans le cas où il y a des champs de type LOB à ne pas retourner.
Elle évite les problèmes de LazyInitializationException et de N + 1 (abordés par la suite). Elle nécessite s'il y a graphe un autre Objet et un mapper voir exemple ci-après.
Exemple
Récupérer une liste de personnes avec sa liste d'adresses mails provenant de la table personne en lien (1,N) avec la table adresse. L'objet à retourner doit être de la forme Objet(long id,String nom, List<String> libelleList). Seuls les champs identifiant et nom de la personne, libellé de l'adresse sont obligatoires. Dans ce cas, on crée un premier objet DTO de type Objet(long id,String nom, String libelle) correspondant directement à une ligne de résultat. Ensuite on crée l'objet correspondant à la demande et enfin on crée un mapper pour transformer la liste d'objets résultats en liste d'objets de la demande. La projection DTO peut donc coûter cher en termes d'implémentation et de maintenance.
DTO est un terme générique et il se trouve qu'on l'utilise aussi dans la couche controller dans le cas de REST, pour cette raison les DTO correspondant à un résultat de requêtes sont suffixés non pas par DTO, mais par Result. De cette manière dans un projet REST + JPA, il n'y a pas de confusion possible entre les objets utilisés dans les deux couches.
I-B-2. Projection entités▲
C'est la projection la plus commune et la plus simple à utiliser. Elle est parfaite dans le cas où on va ensuite appeler une requête de modification. Elle est moins performante que la projection DTO, mais selon le cas c'est négligeable. Elle ne nécessite pas d'implémenter un mapper.
I-C. Statique/Dynamique▲
Selon le type utilisé, il y a un gain en termes de temps (négligeable) et de détection d'erreur de syntaxe.
I-C-1. Requêtes statiques▲
Les requêtes statiques sont invariables au niveau structure, elles ne dépendent pas d'une entrée utilisateur. Elles sont générées (transformation en SQL) au lancement de l'application et mises en cache. À chaque appel de la requête, on gagne donc le temps de génération. Autre avantage si la génération échoue (erreur de syntaxe), le démarrage est immédiatement stoppé, on sait donc tout de suite qu'il y a une erreur.
I-C-2. Requêtes dynamiques▲
Les requêtes dynamiques changent de structure selon les entrées utilisateur. À chaque appel, la requête est générée à nouveau. S’il y a une erreur, on le voit uniquement à l'appel. Les requêtes dynamiques correspondent par exemple au cas d'un formulaire multicritère, car dans ce cas, impossible de connaître la structure à l'avance.
Utiliser une requête dynamique à la place d'une statique n'est pas problématique. Sur l'aspect génération, le gain de temps est négligeable par rapport au temps d'exécution global (génération + requête + mapping) et sur l'aspect détection de l'erreur de syntaxe les tests sont là pour ça. L'aspect implémentation et réutilisabilité est plus important.
I-D. Problèmes JPA▲
I-D-1. LazyInitializationException et N + 1▲
Ces deux problèmes interviennent dans la phase de manipulation du résultat et pour une même cause, l'appel d'une méthode sur une entité en jointure sans que le graphe ne soit chargé. Si aucune connexion avec la base n'est ouverte alors LazyInitializationException sinon N + 1. Ci-dessous un extrait de l'entité Contrat et le service ContratService. ContratService n'est là que pour montrer un exemple des deux problèmes. Les deux méthodes ont le même code, toute la subtilité réside dans l'annotation @Transactional qui garde une connexion ouverte, ce qui va produire un N + 1 à la place d'une LazyInitializationException.
2.
3.
4.
5.
6.
7.
8.
9.
10.
@Entity
@Table(name = "contrat")
public class Contrat {
// ...
@ManyToOne(fetch = FetchType.LAZY)
private Societe societe;
// ...
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
/**
* Classe de service du Contrat
*/
@Service
@RequiredArgsConstructor
@Slf4j
@SuppressWarnings("squid:S4144")
public class ContratService {
private final ContratRepository contratRepository;
@Transactional(readOnly = true)
public void showNPlusOneProblem() {
List<Contrat> contratList = contratRepository.findAll();
contratList.forEach(this::printAdressMail);
}
public void showLazyInitializationException() {
List<Contrat> contratList = contratRepository.findAll();
contratList.forEach(this::printAdressMail);
}
private void printAdressMail(Contrat contrat) {
contrat.getSociete().getAvocat().getAdresseMailSet().forEach(
adresseMail -> log.info(adresseMail.toString())
);
}
}
On peut croire que le problème LazyInitializationException est plus grave, car il est bloquant, mais en fait non, car on le voit et on corrige. Le problème N + 1 est plus sournois, il est plus difficile à détecter, car non bloquant. Si le jeu de données de tests est petit et que l'on n'a pas activé l'affichage des requêtes, on ne va pas le voir. Une fois en production avec des données plus volumineuses, c'est la catastrophe assurée en termes de performance. N peut devenir énorme surtout si comme dans cet exemple, on enchaine plusieurs entités.
I-D-2. Détection▲
Il faut afficher les requêtes générées en développement et en test.
2.
3.
4.
5.
# SPRING
spring:
#JPA
jpa:
show-sql: true
I-D-3. Résolution▲
Dans les deux cas, il faut soit changer le graphe de la requête, soit la réécrire en projection DTO. Il ne faut pas mettre toutes les entités en jointure en fetch = FetchType.EAGER. Mettre tout en EAGER, signifie qu'on va faire systématiquement la jointure donc ça va entraîner des problèmes de performance. Il ne faut pas non plus passer d'un LazyInitializationException à un N + 1 en ajoutant @Transactional.
I-E. Concept de requête efficace▲
À partir des notions, on peut en déduire ce qu'est une requête efficace :
- pas de N + 1 ou de LazyInitializationException à l'utilisation du résultat ;
- chargement du graphe demandé et pas de jointure supplémentaire qui dégrade la performance ;
- projection entités à part si gain de performance important avec la projection DTO en accord avec la notion de simplicité/réutilisabilité.
II. Entités▲
Ce paragraphe peut être à lui seul une documentation complète, on évoque seulement le minimal et notamment la partie en lien avec la génération de requête. Générer en automatique depuis la base donne une base de travail (cas du database first).
CheckList :
-
Annotation JPA
- Pour la classe
2.
@Entity
@Table(name = "table_nom")
- Pour la clé primaire
2.
@Id
@Column(name = "colonne_nom", unique = true, nullable = false)
- Pour les clés étrangères
2.
3.
4.
5.
// de base on force tout à LAZY il faut le préciser, car par défaut ManyToOne et OneToOne sont en EAGER
@OneToOne(fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.LAZY)
@OneToMany(fetch = FetchType.LAZY, mappedBy = "...")
@ManyToMany(fetch = FetchType.LAZY, mappedBy = "...")
-
Pour les autres champs
Sélectionnez1.
2.@Basic@Column(name="colonne_nom") - Annotation lombok sur la classe
2.
3.
4.
@Getter
@Setter
// dans le @ToString on exclut tous les attributs LAZY
@ToString(exclude = {"attributLAZY_1", "attributLAZY_2", ...,"attributLAZY_N"})
Si besoin d'un tutoriel lombok, c'est icituto lombok , sinon générer via l'IDE les getter/setter et le toString (sans les attributs en LAZY).
Astuce
Si votre générateur met les annotations JPA sur les getter, il faut lui renseigner un orm.xml pour lui préciser « access FIELD ».
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
version="2.2">
<persistence-unit-metadata>
<persistence-unit-defaults>
<access>FIELD</access>
</persistence-unit-defaults>
</persistence-unit-metadata>
</entity-mappings>
Afin d'accéder aux attributs des entités, on utilise hibernate-jpamodelgen.
2.
3.
4.
5.
<!-- hibernate jpamodelgen -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
</dependency>
II-A. Modèle de données et graphe▲
Pour présenter les différentes méthodes de requêtage, on utilise le MPD suivant :
Ce modèle correspond à un projet de gestion de contrat :
- un contrat à une liste de statuts et une liste de versions ;
- un contrat à une société qui elle peut avoir plusieurs contrats ;
- une société à deux personnes (président, avocat) ;
- une personne à une liste d'adresses mails.
Ce MPD donne l'implémentation suivante :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
@Entity
@Table(name = "contrat")
@Getter
@Setter
@ToString(exclude = {"societe", "contratStatutSet", "contratVersionSet"})
public class Contrat {
@Id
@Column(name = "id", nullable = false)
private long id;
@Basic
@Column(name = "nom", nullable = false, length = 50)
private String nom;
// FK FIELD
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "fk_societe_id", nullable = false)
private Societe societe;
// JPA FIELD
@OneToMany(fetch = FetchType.LAZY, mappedBy = "contrat")
private Set<ContratStatut> contratStatutSet;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "contrat")
private Set<ContratVersion> contratVersionSet;
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
@Entity
@Table(name = "contrat_statut")
@Getter
@Setter
@ToString(exclude = {"contrat"})
public class ContratStatut {
@Id
@Column(name = "id", nullable = false)
private long id;
@Basic
@Column(name = "statut", nullable = false, length = 50)
private String statut;
@Basic
@Column(name = "actif", nullable = false)
private boolean actif;
// FK FIELD
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "fk_contrat_id", nullable = false)
private Contrat contrat;
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
@Entity
@Table(name = "contrat_version")
@Getter
@Setter
@ToString(exclude = {"contrat"})
public class ContratVersion {
@Id
@Column(name = "id", nullable = false)
private long id;
@Basic
@Column(name = "nom", nullable = false, length = 50)
private String nom;
@Basic
@Column(name = "numero_version", nullable = false)
private int numeroVersion;
@Basic
@Column(name = "actif", nullable = false)
private boolean actif;
// FK FIELD
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "fk_contrat_id", nullable = false)
private Contrat contrat;
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
@Entity
@Table(name = "societe")
@Getter
@Setter
@ToString(exclude = {"contratSet", "avocat", "president"})
public class Societe {
@Id
@Column(name = "id", nullable = false)
private long id;
@Basic
@Column(name = "nom", nullable = false, length = 50)
private String nom;
@Basic
@Column(name = "numero", nullable = false, length = 5)
private String numero;
// FK FIELD
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "fk_personne_avocat_id", nullable = false)
private Personne avocat;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "fk_personne_president_id", nullable = false)
private Personne president;
// JPA FIELD
@OneToMany(fetch = FetchType.LAZY, mappedBy = "societe")
private Set<Contrat> contratSet;
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
@Entity
@Table(name = "personne")
@Getter
@Setter
@ToString(exclude = {"adresseMailSet"})
public class Personne {
@Id
@Column(name = "id", nullable = false)
private long id;
@Basic
@Column(name = "nom", nullable = false, length = 50)
private String nom;
@Basic
@Column(name = "prenom", nullable = false, length = 50)
private String prenom;
@Basic
@Column(name = "avocat", nullable = false)
private boolean avocat;
@Basic
@Column(name = "president", nullable = false)
private boolean president;
// JPA FIELD
@OneToMany(fetch = FetchType.LAZY, mappedBy = "personne")
private Set<AdresseMail> adresseMailSet;
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
@Entity
@Table(name = "adresse_mail")
@Getter
@Setter
@ToString(exclude = {"personne"})
public class AdresseMail {
@Id
@Column(name = "id", nullable = false)
private long id;
@Basic
@Column(name = "libelle", nullable = false, length = 50)
private String libelle;
// FK FIELD
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "fk_personne_id", nullable = false)
private Personne personne;
}
Au niveau graphe, cela donne le schéma suivant :
En pointillé se trouvent les trois sous-graphes qui donnent le graphe complet à récupérer. Ce graphe est utilisé par la suite pour différentes méthodes de requêtage.
III. Repository▲
L'implémentation se base sur les fragments (recommandation Spring Data JPA). L'interface de repository qui étend tous les fragments est une composition de repository. L'interface de repository a accès à toutes les méthodes des fragments. L'interface de repository peut déclarer des méthodes supplémentaires. Une méthode déclarée dans cette interface est non custom, elle ne nécessite pas d'implémentation.
2.
3.
4.
5.
6.
7.
8.
/**
* Interface pour les methodes non custom du repository Contrat
*/
@Repository
public interface ContratRepository extends JpaRepository<Contrat, Long>, JpaSpecificationExecutor<Contrat>,
ContratCustomRepository {
// Déclaration des méthodes non custom
}
JpaRepository étend QueryByExampleExecutor et PagingAndSortingRepository qui étend CrudRepository. Avec cette implémentation, il y a déjà un bon nombre de méthodes à disposition ne nécessitant aucune implémentation. Les autres méthodes qui sont custom, car elles nécessitent une implémentation sont dans l'interface ContratCustomRepository.
2.
3.
4.
5.
6.
/**
* Interface pour les methodes custom du repository Contrat
*/
public interface ContratCustomRepository {
// Déclaration des méthodes custom
}
Une classe abstraite générique contenant deux autres méthodes est à disposition pour les repository custom.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
/**
* Classe generique pour les methodes custom de repository
*/
@Slf4j
public abstract class AbstractCustomRepository<T> {
/**
* Permet de recuperer une liste d entites en choisissant le graphe et la specification
*
* @param subGraphUtilList
* @param specification
* @return une liste d entites en choisissant le graphe
*/
public List<T> findAllWithGraphAndSpecification(List<SubGraphUtil> subGraphUtilList,
@Nullable Specification<T> specification) {
// ...
}
/**
* Permet de recuperer une entite en choisissant le graphe et la specification
*
* @param subGraphUtilList
* @param specification
* @return une entite en choisissant le graphe
*/
public Optional<T> findOneWithGraphAndSpecification(List<SubGraphUtil> subGraphUtilList,
@Nullable Specification<T> specification) {
//...
}
}
SubGraphUtil est une classe utilitaire servant à créer les graphes.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
/**
* Classe utilitaire pour creer les graphes
*/
@Getter
@Setter
public final class SubGraphUtil {
private String name;
private List<SubGraphUtil> subGraphUtilList;
public SubGraphUtil(String name) {
this.name = name;
}
}
La classe d'implémentation du repository custom étend la classe abstraite générique pour bénéficier des méthodes.
2.
3.
4.
5.
6.
7.
8.
/**
* Classe d implementation des methodes custom du repository contrat
*/
@SuppressWarnings({"unused","squid:S1192"})
public class ContratCustomRepositoryImpl extends AbstractCustomRepository<Contrat>
implements ContratCustomRepository {
// Implémentation des méthodes custom
}
La structure complète est en place.
IV. Méthodes de requêtage▲
Pour les méthodes ne pouvant réaliser que des choses simples, on précise le type de demande possible.
Pour les méthodes pouvant réaliser des choses complexes et afin de les comparer, on utilise quatre demandes :
- Graphe, projection entités ;
- Graphe, projection entités, clause where statique ;
- Graphe, projection DTO, clause where statique ;
- Graphe, projection entités, clause where dynamique.
Le graphe commun est celui du paragraphe Modèle de données et graphe.
La clause where est un AND entre trois critères :
- In sur l'identifiant contrat ;
- Equal sur le booléen actif de ContratVersion ;
- EndWithIgnoreCase sur le libellé de l'adresse mail de l'avocat.
L'objet commun ContratCriteria pour clause where dynamique comporte les trois mêmes attributs que ceux de la clause where statique :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
@Data
@Builder
public class ContratCriteria {
// contrat
private List<Long> contratIdList;
// contratVersion
private Boolean contratVersionActif;
// societe
// adresseMail
private String suffixAvocatMail;
}
L'objet commun ContratProjectionResult pour la projection DTO :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
@Value
public class ContratProjectionResult {
// contrat
long contratId;
String contratNom;
// contratVersion
long contratVersionId;
int contratVersionNumero;
// societe
long societeId;
String societeNom;
// personne
String avocatNom;
String presidentNom;
// adresseMail
String avocatMail;
}
Pour toutes les méthodes, des exemples sont présents dans le paragraphe Tests unitaires.
IV-A. CrudRepository query▲
Type de demande :
- récupérer le contrat avec tous ses champs par son identifiant ;
- récupérer les contrats avec tous leurs champs.
Avantages :
- aucune implémentation.
Inconvénients :
- statique ;
- sans graphe ;
- projection entités sur la root uniquement ;
- sans projection DTO.
IV-B. Derived query▲
Type de demande :
- récupérer les contrats avec tous leurs champs par l'identifiant société ;
- récupérer les contrats avec tous leurs champs dont le nom de la société commence par ;
- récupérer les contrats avec les champs id, nom par l'identifiant société ;
- récupérer les contrats avec le champ id par l'identifiant société.
Avantages :
- pas d'implémentation, juste une déclaration (à part les DTO si projection DTO) ;
- projection DTO dynamique.
Inconvénients :
- statique ;
- sans graphe ;
- projection entités sur la root uniquement ;
- projection DTO sur la root uniquement (sinon génère du N + 1).
Les demandes 3 et 4 sont traitées par une même méthode gérant la projection DTO dynamique.
Implémentation :
Déclaration dans ContratRepository, car non custom.
2.
3.
4.
5.
List<Contrat> findBySocieteId(long societeId);
List<Contrat> findBySocieteNomStartsWith(String prefixSociete);
<T> List<T> findBySocieteId(long societeId, Class<T> type);
Création des Classes correspondantes aux projections DTO. Spring Data JPA propose l'utilisation de l'annotation @Value de lombok. À partir de JAVA 14, on peut utiliser un record.
2.
3.
4.
5.
6.
@Value
public class ContratIdNameResult {
long id;
String nom;
}
2.
3.
4.
5.
@Value
public class IdResult {
long id;
}
IV-C. Example query▲
Type de demande :
- récupérer les contrats selon le contrat en exemple.
Avantages :
- aucune implémentation ;
- dynamique ;
- nombreux matcher existant.
Inconvénients :
- sans graphe ;
- projection entités sur la root uniquement ;
- sans projection DTO.
IV-D. JPQL query▲
Avantages :
- graphe ;
- projection entités et DTO.
Inconvénients :
- dynamique possible, mais à proscrire (illisible, inmaintenable) ;
- implémentation non réutilisable.
Implémentation :
déclaration dans ContratCustomRepository, car custom.
2.
3.
4.
5.
6.
7.
8.
List<Contrat> findJPQLGraph();
List<Contrat> findJPQLGraphWhere(List<Long> contratIdList, boolean contratVersionActif,
String suffixMail);
List<ContratProjectionResult> findJPQLGraphWhereProjection(List<Long> contratIdList,
boolean contratVersionActif,
String suffixMail);
Implémentation dans ContratCustomRepositoryImpl
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
@Override
public List<Contrat> findJPQLGraph() {
TypedQuery<Contrat> typedQuery = entityManager.createQuery("SELECT DISTINCT c FROM Contrat c "
+ "JOIN FETCH c.contratVersionSet cv "
+ "JOIN FETCH c.societe s "
+ "JOIN FETCH s.avocat a "
+ "JOIN FETCH a.adresseMailSet am "
+ "JOIN FETCH s.president p ",
Contrat.class
);
return typedQuery.getResultList();
}
@Override
public List<Contrat> findJPQLGraphWhere(List<Long> contratIdList, boolean contratVersionActif,
String suffixMail) {
TypedQuery<Contrat> typedQuery = entityManager.createQuery("SELECT DISTINCT c FROM Contrat c "
+ "JOIN FETCH c.contratVersionSet cv "
+ "JOIN FETCH c.societe s "
+ "JOIN FETCH s.avocat a "
+ "JOIN FETCH a.adresseMailSet am "
+ "JOIN FETCH s.president p "
+ "WHERE c.id IN (:contratIdList) "
+ "AND cv.actif = :contratVersionActif "
+ "AND UPPER(am.libelle) LIKE :suffixMail ",
Contrat.class
);
typedQuery.setParameter("contratIdList", contratIdList);
typedQuery.setParameter("contratVersionActif", contratVersionActif);
typedQuery.setParameter("suffixMail", "%" + suffixMail.toUpperCase());
return typedQuery.getResultList();
}
@Override
public List<ContratProjectionResult> findJPQLGraphWhereProjection(List<Long> contratIdList,
boolean contratVersionActif,
String suffixMail) {
TypedQuery<ContratProjectionResult> typedQuery = entityManager.createQuery("SELECT NEW fr.agoero" +
".result" +
".ContratProjectionResult("
+ "c.id,"
+ "c.nom,"
+ "cv.id,"
+ "cv.numeroVersion,"
+ "s.id,"
+ "s.nom,"
+ "a.nom,"
+ "p.nom,"
+ "am.libelle)"
+ "FROM Contrat c "
+ "JOIN c.contratVersionSet cv "
+ "JOIN c.societe s "
+ "JOIN s.avocat a "
+ "JOIN a.adresseMailSet am "
+ "JOIN s.president p "
+ "WHERE c.id IN (:contratIdList) "
+ "AND cv.actif = :contratVersionActif "
+ "AND UPPER(am.libelle) LIKE :suffixMail ",
ContratProjectionResult.class
);
typedQuery.setParameter("contratIdList", contratIdList);
typedQuery.setParameter("contratVersionActif", contratVersionActif);
typedQuery.setParameter("suffixMail", "%" + suffixMail.toUpperCase());
return typedQuery.getResultList();
}
Une autre implémentation moins verbeuse est possible en utilisant @Query.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
/**
* Interface pour les methodes non custom du repository Contrat
*/
@Repository
public interface ContratRepository extends JpaRepository<Contrat, Long>, JpaSpecificationExecutor<Contrat>,
ContratCustomRepository {
@Query("SELECT DISTINCT c FROM Contrat c "
+ "JOIN FETCH c.contratVersionSet cv "
+ "JOIN FETCH c.societe s "
+ "JOIN FETCH s.avocat a "
+ "JOIN FETCH a.adresseMailSet am "
+ "JOIN FETCH s.president p ")
List<Contrat> findJPQLGraph();
}
Avec cette annotation, plus besoin d'implémentation dans ContratCustomRepositoryImpl, mais inconvénients :
- non-respect des fragments, car obligation de déclarer sur l'interface non custom ;
- plus de point d'arrêt possible ;
- aucun ajout de code possible.
IV-E. Criteria query▲
Avantages :
- graphe ;
- projection entités et DTO ;
- dynamique.
Inconvénients :
- implémentation non réutilisable ;
- implémentation verbeuse.
Implémentation :
déclaration dans ContratCustomRepository, car custom.
2.
3.
4.
5.
6.
7.
8.
9.
10.
List<Contrat> findCriteriaGraph();
List<Contrat> findCriteriaGraphWhere(List<Long> contratIdList, boolean contratVersionActif,
String suffixMail);
List<ContratProjectionResult> findCriteriaGraphWhereProjection(List<Long> contratIdList,
boolean contratVersionActif,
String suffixMail);
List<Contrat> findCriteriaGraphWhereDynamic(ContratCriteria contratCriteria);
Implémentation dans ContratCustomRepositoryImpl
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
@Override
public List<Contrat> findCriteriaGraph() {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Contrat> criteriaQuery = criteriaBuilder.createQuery(Contrat.class);
Root<Contrat> contratRoot = criteriaQuery.from(Contrat.class);
contratRoot.fetch(Contrat_.CONTRAT_VERSION_SET);
Fetch<Contrat, Societe> societeFetch = contratRoot.fetch(Contrat_.SOCIETE);
Fetch<Societe, Personne> avocatFetch = societeFetch.fetch(Societe_.AVOCAT);
avocatFetch.fetch(Personne_.ADRESSE_MAIL_SET);
societeFetch.fetch(Societe_.PRESIDENT);
criteriaQuery.distinct(true);
return entityManager.createQuery(criteriaQuery).getResultList();
}
@Override
@SuppressWarnings("unchecked")
public List<Contrat> findCriteriaGraphWhere(List<Long> contratIdList, boolean contratVersionActif,
String suffixMail) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Contrat> criteriaQuery = criteriaBuilder.createQuery(Contrat.class);
criteriaQuery.distinct(true);
Root<Contrat> contratRoot = criteriaQuery.from(Contrat.class);
Fetch<Contrat, ContratVersion> contratVersionFetch = contratRoot.fetch(Contrat_.CONTRAT_VERSION_SET);
Join<Contrat, ContratVersion> contratVersionJoin =
(Join<Contrat, ContratVersion>) contratVersionFetch;
Fetch<Contrat, Societe> societeFetch = contratRoot.fetch(Contrat_.SOCIETE);
Fetch<Societe, Personne> avocatFetch = societeFetch.fetch(Societe_.AVOCAT);
Fetch<Personne, AdresseMail> adresseMailFetch = avocatFetch.fetch(Personne_.ADRESSE_MAIL_SET);
Join<Personne, AdresseMail> adresseMailJoin = (Join<Personne, AdresseMail>) adresseMailFetch;
societeFetch.fetch(Societe_.PRESIDENT);
// predicate
Predicate contratIdIn = contratRoot.get(Contrat_.ID).in(contratIdList);
Predicate contratVersionActifEqual =
criteriaBuilder.equal(contratVersionJoin.get(ContratVersion_.ACTIF),
contratVersionActif);
Predicate adresseMailEndWithIgnoreCase =
criteriaBuilder.like(criteriaBuilder.upper(adresseMailJoin.get(AdresseMail_.LIBELLE)),
"%" + suffixMail.toUpperCase());
criteriaQuery.where(criteriaBuilder.and(contratIdIn, contratVersionActifEqual,
adresseMailEndWithIgnoreCase));
return entityManager.createQuery(criteriaQuery).getResultList();
}
@Override
public List<ContratProjectionResult> findCriteriaGraphWhereProjection(List<Long> contratIdList,
boolean contratVersionActif,
String suffixMail) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<ContratProjectionResult> criteriaQuery =
criteriaBuilder.createQuery(ContratProjectionResult.class);
Root<Contrat> contratRoot = criteriaQuery.from(Contrat.class);
Join<Contrat, ContratVersion> contratVersionJoin = contratRoot.join(Contrat_.CONTRAT_VERSION_SET);
Join<Contrat, Societe> societeJoin = contratRoot.join(Contrat_.SOCIETE);
Join<Societe, Personne> avocatJoin = societeJoin.join(Societe_.AVOCAT);
Join<Personne, AdresseMail> adresseMailJoin = avocatJoin.join(Personne_.ADRESSE_MAIL_SET);
Join<Societe, Personne> presidentJoin = societeJoin.join(Societe_.PRESIDENT);
criteriaQuery.select(criteriaBuilder.construct(ContratProjectionResult.class,
contratRoot.get(Contrat_.ID),
contratRoot.get(Contrat_.NOM),
contratVersionJoin.get(ContratVersion_.ID),
contratVersionJoin.get(ContratVersion_.NUMERO_VERSION),
societeJoin.get(Societe_.ID),
societeJoin.get(Societe_.NOM),
avocatJoin.get(Personne_.NOM),
presidentJoin.get(Personne_.NOM),
adresseMailJoin.get(AdresseMail_.LIBELLE)
)
);
// predicate
Predicate contratIdIn = contratRoot.get(Contrat_.ID).in(contratIdList);
Predicate contratVersionActifEqual =
criteriaBuilder.equal(contratVersionJoin.get(ContratVersion_.ACTIF),
contratVersionActif);
Predicate adresseMailEndWithIgnoreCase =
criteriaBuilder.like(criteriaBuilder.upper(adresseMailJoin.get(AdresseMail_.LIBELLE)),
"%" + suffixMail.toUpperCase());
criteriaQuery.where(criteriaBuilder.and(contratIdIn, contratVersionActifEqual,
adresseMailEndWithIgnoreCase));
return entityManager.createQuery(criteriaQuery).getResultList();
}
@SuppressWarnings("unchecked")
@Override
public List<Contrat> findCriteriaGraphWhereDynamic(ContratCriteria contratCriteria) {
List<Predicate> predicateList = new ArrayList<>();
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Contrat> criteriaQuery = criteriaBuilder.createQuery(Contrat.class);
criteriaQuery.distinct(true);
Root<Contrat> contratRoot = criteriaQuery.from(Contrat.class);
Fetch<Contrat, ContratVersion> contratVersionFetch = contratRoot.fetch(Contrat_.CONTRAT_VERSION_SET);
Fetch<Contrat, Societe> societeFetch = contratRoot.fetch(Contrat_.SOCIETE);
Fetch<Societe, Personne> avocatFetch = societeFetch.fetch(Societe_.AVOCAT);
Fetch<Personne, AdresseMail> adresseMailFetch = avocatFetch.fetch(Personne_.ADRESSE_MAIL_SET);
societeFetch.fetch(Societe_.PRESIDENT);
// predicate
if (isNotEmpty(contratCriteria.getContratIdList())) {
Predicate contratIdIn = contratRoot.get(Contrat_.ID).in(contratCriteria.getContratIdList());
predicateList.add(contratIdIn);
}
if (contratCriteria.getContratVersionActif() != null) {
Join<Contrat, ContratVersion> contratVersionJoin =
(Join<Contrat, ContratVersion>) contratVersionFetch;
Predicate contratVersionActifEqual =
criteriaBuilder.equal(contratVersionJoin.get(ContratVersion_.ACTIF),
contratCriteria.getContratVersionActif());
predicateList.add(contratVersionActifEqual);
}
if (isNotBlank(contratCriteria.getSuffixAvocatMail())) {
Join<Personne, AdresseMail> adresseMailJoin = (Join<Personne, AdresseMail>) adresseMailFetch;
Predicate adresseMailEndWithIgnoreCase =
criteriaBuilder.like(criteriaBuilder.upper(adresseMailJoin.get(AdresseMail_.LIBELLE)),
"%" + contratCriteria.getSuffixAvocatMail().toUpperCase());
predicateList.add(adresseMailEndWithIgnoreCase);
}
criteriaQuery.where(predicateList.toArray(new Predicate[]{}));
return entityManager.createQuery(criteriaQuery).getResultList();
}
IV-F. Native query▲
À éviter à part dans le cas où la requête comporte quelque chose de spécifique à la base de données. Un seul cas pour faire comprendre à quel point c'est plus verbeux.
Implémentation :
déclaration dans ContratCustomRepository, car custom.
2.
3.
List<ContratProjectionResult> findNativeGraphWhereProjection(List<Long> contratIdList,
boolean contratVersionActif,
String suffixMail);
Implémentation dans ContratCustomRepositoryImpl
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
@Override
@SuppressWarnings("unchecked")
public List<ContratProjectionResult> findNativeGraphWhereProjection(List<Long> contratIdList,
boolean contratVersionActif,
String suffixMail) {
Query query = entityManager.createNativeQuery("SELECT "
+ "c.id AS contratId, "
+ "c.nom AS contratNom, "
+ "cv.id AS contratVersionId, "
+ "cv.numero_version AS contratVersionNumero, "
+ "s.id AS societeId, "
+ "s.nom AS societeNom, "
+ "a.nom AS avocatNom, "
+ "p.nom AS presidentNom, "
+ "am.libelle AS avocatMail "
// from
+ "FROM contrat c "
+ "JOIN contrat_version cv ON c.id = cv.fk_contrat_id "
+ "JOIN societe s ON c.fk_societe_id = s.id "
+ "JOIN personne p ON s.fk_personne_president_id = p.id "
+ "JOIN personne a ON s.fk_personne_avocat_id = a.id "
+ "JOIN adresse_mail am ON a.id = am.fk_personne_id "
// where
+ "WHERE c.id IN (:contratIdList) "
+ "AND cv.actif = :contratVersionActif "
+ "AND UPPER(am.libelle) LIKE :suffixMail",
"ContratProjectionMapping");
query.setParameter("contratIdList", contratIdList);
query.setParameter("contratVersionActif", contratVersionActif);
query.setParameter("suffixMail", "%" + suffixMail.toUpperCase());
return query.getResultList();
}
Ici l'implémentation doit ajouter du code pour le mapping, car il n'y a pas d'équivalent au criteriaBuilder.construct ni au SELECT NEW.
On déclare un mapping via la classe ContratProjectionMapping :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
@MappedSuperclass
@SqlResultSetMapping(
name = "ContratProjectionMapping",
classes = @ConstructorResult(
targetClass = ContratProjectionResult.class,
columns = {
@ColumnResult(name = "contratId", type = Long.class),
@ColumnResult(name = "contratNom", type = String.class),
@ColumnResult(name = "contratVersionId", type = Long.class),
@ColumnResult(name = "contratVersionNumero", type = Integer.class),
@ColumnResult(name = "societeId", type = Long.class),
@ColumnResult(name = "societeNom", type = String.class),
@ColumnResult(name = "avocatNom", type = String.class),
@ColumnResult(name = "presidentNom", type = String.class),
@ColumnResult(name = "avocatMail", type = String.class)
}
)
)
public class ContratProjectionMapping {
}
Il est possible de déclarer le @SqlResultSetMapping directement sur l'entité, mais c'est de la « pollution d'entité ». S’il y a trois Native query avec trois mappings différents l'entité est illisible.
Astuce
Certains proposent d'utiliser comme ici une classe à part, mais utilisent un @Entity ce qui apporte un lot d'inconvénients :
- déclaration obligatoire d'un champ identifiant avec son annotation @Id ;
- pose des problèmes au démarrage de l'application à cause du ddl-auto.
Si le ddl-auto est en validate, l'application ne démarre pas :
2.
Caused by: org.hibernate.tool.schema.spi.SchemaManagementException:
Schema-validation: missing table [contrat_projection_mapping]
Si le ddl-auto est en update, Hibernate crée la table en base (ou essaie selon les droits) :
2.
3.
4.
5.
6.
Hibernate:
create table contrat_projection_mapping (
id int8 not null,
primary key (id)
)
Avec @MappedSuperclass, il n'y a pas de problème.
IV-G. AbstractCustomRepository query▲
Cette méthode de requêtage n'est pas classique, c'est une amélioration de la méthode par spécification.
Traduction libre de la documentation de Spring Data JPA :
JPA 2 introduit une API de critères que vous pouvez utiliser pour créer des requêtes dynamiques. En écrivant un critère, vous définissez la clause where d'une requête. Les spécifications peuvent facilement être utilisées pour créer un ensemble extensible de prédicats sur une entité. Cet ensemble de prédicats peut ensuite être combiné et utilisé avec JpaRepository sans avoir besoin de déclarer une requête (méthode) pour chaque combinaison.
On peut voir ça comme une amélioration de l'API criteria en apportant la réutilisabilisation.
Pourtant en l'état, il y a trois inconvénients :
- sans graphe ;
- sans critère sur une entité en jointure ;
- projection entités uniquement.
L'amélioration « répare » deux des trois inconvénients, seul l'aspect projection DTO reste infaisable en l'état. Les deux méthodes de AbstractCustomRepository permettent de récupérer une entité ou une liste d'entités en choisissant le graphe et les critères. On peut appeler avec une spécification nulle et dans ce cas, seule la partie graphe est prise en compte. Pas besoin d'implémenter le cas où il y a spécification sans graphe, car c'est ce que fait déjà JpaSpecificationExecutor. La manière d'implémenter les prédicats décrits ci-après permet d'appeler AbstractCustomRepository ou directement JpaSpecificationExecutor. Autrement dit, on peut faire toutes les requêtes avec projection entités en statique et en dynamique sans avoir à implémenter du nouveau code dans la couche repository.
Avantages :
- graphe ;
- dynamique ;
- pas d'implémentation dans ContratCustomRepositoryImpl ;
- réutilisable ;
- maintenable.
Inconvénients :
- implémentation des graphes et des spécifications, mais réutilisable :
- projection entités uniquement.
Implémentation :
déclaration dans ContratCustomRepository, car custom.
2.
List<Contrat> findAllWithGraphAndSpecification(List<SubGraphUtil> subGraphUtilList,
Specification<Contrat> specification);
Implémentation partie graphe :
pour les graphes, on se sert de l'utilitaire SubGraphUtil pour chaque entité.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
/**
* Classe de graphes pour Contrat
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ContratGraph {
/**
* Permet de construire le sous-graphe ContratVersion
*
* @return le sous-graphe ContratVersion
*/
public static SubGraphUtil getContratVersionGraph() {
return new SubGraphUtil(Contrat_.CONTRAT_VERSION_SET);
}
/**
* Permet de construire le sous-graphe Societe
*
* @return le sous-graphe Societe
*/
public static SubGraphUtil getSocieteGraph() {
return new SubGraphUtil(Contrat_.SOCIETE);
}
/**
* Permet de construire le sous-graphe Societe President Avocat AdresseMail
*
* @return le sousgraphe Avocat
*/
public static SubGraphUtil getSocPdtAvoMailGraph() {
SubGraphUtil subGraphSocPdtAvoMail = getSocieteGraph();
subGraphSocPdtAvoMail.setSubGraphUtilList(Arrays.asList(SocieteGraph.getPresidentGraph(),
SocieteGraph.getAvocatMailGraph()));
return subGraphSocPdtAvoMail;
}
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
/**
* Classe de graphes pour Societe
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class SocieteGraph {
/**
* Permet de construire le sous-graphe Avocat
*
* @return le sous-graphe Avocat
*/
public static SubGraphUtil getAvocatGraph() {
return new SubGraphUtil(Societe_.AVOCAT);
}
/**
* Permet de construire le sous-graphe President
*
* @return le sous-graphe President
*/
public static SubGraphUtil getPresidentGraph() {
return new SubGraphUtil(Societe_.PRESIDENT);
}
/**
* Permet de construire le sous-graphe AvocatMail
*
* @return le sous-graphe AvocatMail
*/
public static SubGraphUtil getAvocatMailGraph() {
SubGraphUtil subGraphAvocat = SocieteGraph.getAvocatGraph();
SubGraphUtil subGraphAdresseMail = PersonGraph.getAdresseEmailGraph();
subGraphAvocat.setSubGraphUtilList(Collections.singletonList(subGraphAdresseMail));
return subGraphAvocat;
}
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
/**
* Classe de graphes pour Personne
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class PersonGraph {
/**
* Permet de construire le sous-graphe AdresseEmail
*
* @return le sousgraphe AdresseEmail
*/
public static SubGraphUtil getAdresseEmailGraph() {
return new SubGraphUtil(Personne_.ADRESSE_MAIL_SET);
}
}
Pour les sous-graphes directs, c'est très simple :
- Contrat vers contratVersion : correspond au sous-graphe rouge ;
- Contrat vers Societe ;
- Societe vers Personne (avocat) ;
- Societe vers Personne (président) : correspond au sous-graphe vert ;
- Personne vers AdresseMail.
Pour créer les autres sous-graphes plus complexes, on combine. On peut implémenter la combinaison directement dans les classes de graphe pour réutilisation ou combiner dans la méthode juste avant l'appel à AbstractCustomRepository. Pour montrer les deux options, le graphe final n'est pas présent, il est uniquement présent dans la classe de tests (voir paragraphe Tests unitaires).
Pour obtenir le sous-graphe bleu (getAvocatMailGraph), on ajoute « personne vers adresseMail » dans « société vers personne (avocat) ».
Pour obtenir le sous-graphe vert + bleu (getSocPdtAvoMailGraph), dans le contrat, on part de « contrat vers société » et on ajoute une liste contenant le vert et le bleu.
Le graphe complet déclaré dans la classe de tests est Arrays.asList(ContratGraph.getContratVersionGraph(),ContratGraph.getSocPdtAvoMailGraph()).
Implémentation partie spécification
Si on suit l'exemple de la documentation de Spring Data JPA, la méthode crée le prédicat et retourne la spécification. De cette façon, le prédicat n'est pas accessible à l'extérieur. Dans le but de pouvoir réutiliser le prédicat sur une entité en jointure, on sépare en deux méthodes, l'une renvoyant le prédicat et l'autre appelant la première renvoyant la spécification.
Le prédicat peut donc être récupéré depuis la spécification d'une autre entité. L'autre entité a besoin de déclarer un Join, mais elle peut être une Root ou un Join. De plus si l'entité de départ se sert de plusieurs prédicats d'une même entité en jointure, on a deux Join, la requête générée aussi.
Pour simplifier l'implémentation, une classe utilitaire générique réalise la récupération des Join que ce soit à partir d'une Root ou d'un Join en le créant ou en renvoyant un existant.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
@SuppressWarnings("rawtypes")
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class SpecificationUtil {
public static <X> Join getJoin(final Root<X> root, final String attributeName) {
final Optional<Join<X, ?>> join = root.getJoins()
.stream()
.filter(equalAttributeNamePredicate(attributeName))
.findFirst();
return join.orElseGet(() -> root.join(attributeName));
}
public static <X, Z> Join getJoin(final Join<X, Z> initialJoin, final String attributeName) {
final Optional<Join<Z, ?>> join = initialJoin.getJoins()
.stream()
.filter(equalAttributeNamePredicate(attributeName))
.findFirst();
return join.orElseGet(() -> initialJoin.join(attributeName));
}
public static <X> Specification<X> initOrAndSpecification(Specification<X> specification,
Specification<X> specificationToAnd) {
return specification == null ? specificationToAnd : specification.and(specificationToAnd);
}
private static <T> Predicate<Join<T, ?>> equalAttributeNamePredicate(String attributeName) {
return join -> join.getAttribute()
.getName()
.equals(attributeName);
}
}
La méthode initOrAndSpecification sert pour les requêtes dynamiques, pas pour la déclaration des prédicats.
Classes de spécification :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
/**
* Classe de specification pour Contrat
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings({"rawtypes", "unchecked"})
public final class ContratSpecification {
public static Specification<Contrat> idIn(final List<Long> contratIdList) {
return (root, query, criteriaBuilder) -> getIdIn(contratIdList, root, criteriaBuilder);
}
static Predicate getIdIn(final List<Long> contratIdList, final From from,
final CriteriaBuilder criteriaBuilder) {
return criteriaBuilder.in(from.get(Contrat_.ID)).value(contratIdList);
}
public static Specification<Contrat> contratVersionActifEqual(final boolean active) {
return (root, query, criteriaBuilder) -> {
final Join<Contrat, ContratVersion> contratVersionJoin = getJoin(root,
Contrat_.CONTRAT_VERSION_SET);
return ContratVersionSpecification.getActifEqual(active, contratVersionJoin, criteriaBuilder);
};
}
public static Specification<Contrat> adresseMailLibelleEndWithIgnoreCase(final String rolePersonne,
final String suffixMail) {
return (root, query, criteriaBuilder) -> {
final Join<Contrat, Societe> societeJoin = getJoin(root, Contrat_.SOCIETE);
final Join<Societe, Personne> personneJoin = getJoin(societeJoin, rolePersonne);
final Join<Personne, AdresseMail> adresseMAilJoin = getJoin(personneJoin,
Personne_.ADRESSE_MAIL_SET);
return AdresseMailSpecification.getLibelleEndWithIgnoreCase(suffixMail, adresseMAilJoin,
criteriaBuilder);
};
}
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
/**
* Classe de specification pour ContratVersion
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings("rawtypes")
public final class ContratVersionSpecification {
public static Specification<ContratVersion> actifEqual(final boolean active) {
return (root, query, criteriaBuilder) ->
getActifEqual(active, root, criteriaBuilder);
}
static Predicate getActifEqual(final boolean actif, final From from,
final CriteriaBuilder criteriaBuilder) {
return criteriaBuilder.equal(from.get(ContratVersion_.ACTIF), actif);
}
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
/**
* Classe de specification pour AdresseMail
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings({"rawtypes", "unchecked"})
public final class AdresseMailSpecification {
public static Specification<Contrat> libelleEndWithIgnoreCase(final String suffixMail) {
return (root, query, criteriaBuilder) -> getLibelleEndWithIgnoreCase(suffixMail, root,
criteriaBuilder);
}
static Predicate getLibelleEndWithIgnoreCase(final String suffixMail, final From from,
final CriteriaBuilder criteriaBuilder) {
return criteriaBuilder.like(criteriaBuilder.upper(from.get(AdresseMail_.LIBELLE)),
"%" + suffixMail.toUpperCase());
}
}
V. Tests unitaires▲
Sert à montrer comment appeler les méthodes de requêtage et à vérifier le concept de requête efficace. Pour vérifier que la partie du graphe nécessaire est chargée, on appelle une méthode de type assertXLoad. Le principe est que si le graphe est bien chargé, alors pas de LazyInitializationException.
Pour vérifier qu'une partie du graphe n'est pas chargée inutilement, on appelle une méthode de type assertXNotLoad.
Le principe est que si le graphe est chargé inutilement, alors il faut une LazyInitializationException.
Assert pour le sous-graphe société :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
private void assertSocieteNotLoaded(Societe societe) {
assertThrows(LazyInitializationException.class, () -> societe.setId(1L), "graph societe is loaded");
}
private void assertSocieteLoaded(Societe societe) {
try {
societe.setId(1L);
} catch (LazyInitializationException e) {
fail("graph societe not loaded");
}
}
Assert pour les requêtes récupérant le graphe :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
private void assertGraph(List<Contrat> contratList) {
contratList.forEach(contrat -> {
assertSocieteLoaded(contrat.getSociete());
assertPersonneLoaded(contrat.getSociete().getAvocat());
assertAdresseMailLoaded(contrat.getSociete().getAvocat().getAdresseMailSet());
assertPersonneLoaded(contrat.getSociete().getPresident());
assertAdresseMailNotLoaded(contrat.getSociete().getPresident().getAdresseMailSet());
assertContratVersionLoaded(contrat.getContratVersionSet());
assertContratStatutNotLoaded(contrat.getContratStatutSet());
}
);
}
Les tests unitaires utilisent une base H2 en mémoire. Hibernate génère le MPD à partir des entités (ddl-auto: create).
Configuration :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
# SPRING
spring:
# DATASOURCE
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driver-class-name: org.h2.Driver
#JPA
jpa:
database: h2
hibernate:
use-new-id-generator-mappings: true
ddl-auto: create
open-in-view: false
properties:
hibernate:
format_sql: true
use_sql_comments: false
dialect: org.hibernate.dialect.H2Dialect
show-sql: true
Spring Boot insère les données depuis un script sql.
Traduction libre de la documentation “How-to” Guides de Spring Boot :
Spring Boot peut créer automatiquement le schéma (scripts DDL) de votre DataSource et l'initialiser (scripts DML).
Il charge à partir des emplacements de chemin de classe racine standard : schema.sql et data.sql.
La partie schéma est faite par Hibernate, il n'y a pas besoin d'un script schema.sql. Reste à fournir un fichier data.sql pour les données.
Jeu de données src/test/resources/data.sql :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
-- personne
INSERT INTO personne (id, nom, prenom, avocat, president)
VALUES (1, 'avocat1nom', 'avocat1prenom', true, false);
INSERT INTO personne (id, nom, prenom, avocat, president)
VALUES (2, 'president1nom', 'president1prenom', false, true);
INSERT INTO personne (id, nom, prenom, avocat, president)
VALUES (3, 'avocat2nom', 'avocat2prenom', true, false);
INSERT INTO personne (id, nom, prenom, avocat, president)
VALUES (4, 'president2nom', 'president2prenom', false, true);
-- adresse_mail
INSERT INTO adresse_mail (id, libelle, fk_personne_id)
VALUES (1, 'avocat1@societe1.fr', 1);
INSERT INTO adresse_mail (id, libelle, fk_personne_id)
VALUES (2, 'avocat1@soc1.com', 1);
INSERT INTO adresse_mail (id, libelle, fk_personne_id)
VALUES (3, 'avocat1@societe1.com', 1);
INSERT INTO adresse_mail (id, libelle, fk_personne_id)
VALUES (4, 'president1@societe1.fr', 2);
INSERT INTO adresse_mail (id, libelle, fk_personne_id)
VALUES (5, 'president1@soc1.com', 2);
INSERT INTO adresse_mail (id, libelle, fk_personne_id)
VALUES (6, 'president2@soc2.com', 4);
INSERT INTO adresse_mail (id, libelle, fk_personne_id)
VALUES (7, 'avocat2@societe2.com', 3);
-- societe
INSERT INTO societe (id, nom, numero, fk_personne_avocat_id, fk_personne_president_id)
VALUES (1, 'societe1', '1111', 1, 2);
INSERT INTO societe (id, nom, numero, fk_personne_avocat_id, fk_personne_president_id)
VALUES (2, 'societe2', '2222', 3, 4);
-- contrat
INSERT INTO contrat (id, nom, fk_societe_id)
VALUES (1, 'contrat1', 1);
INSERT INTO contrat (id, nom, fk_societe_id)
VALUES (2, 'contrat2', 1);
INSERT INTO contrat (id, nom, fk_societe_id)
VALUES (3, 'contrat3', 2);
-- contrat_statut
INSERT INTO contrat_statut (id, statut, actif, fk_contrat_id)
VALUES (1, 'en_cours', false, 1);
INSERT INTO contrat_statut (id, statut, actif, fk_contrat_id)
VALUES (2, 'annule', true, 1);
INSERT INTO contrat_statut (id, statut, actif, fk_contrat_id)
VALUES (3, 'en_cours', false, 2);
INSERT INTO contrat_statut (id, statut, actif, fk_contrat_id)
VALUES (4, 'ternime', true, 2);
INSERT INTO contrat_statut (id, statut, actif, fk_contrat_id)
VALUES (5, 'ternime', true, 3);
-- contrat_version
INSERT INTO contrat_version (id, nom, numero_version, actif, fk_contrat_id)
VALUES (1, 'version1', 1, false, 1);
INSERT INTO contrat_version (id, nom, numero_version, actif, fk_contrat_id)
VALUES (2, 'version2', 2, true, 1);
INSERT INTO contrat_version (id, nom, numero_version, actif, fk_contrat_id)
VALUES (3, 'version1', 1, true, 2);
INSERT INTO contrat_version (id, nom, numero_version, actif, fk_contrat_id)
VALUES (4, 'version1', 1, true, 3);
Classe de tests unitaires du repository :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
198.
199.
200.
201.
202.
203.
204.
205.
206.
207.
208.
209.
210.
211.
212.
213.
214.
215.
216.
217.
218.
219.
220.
221.
222.
223.
224.
225.
226.
227.
228.
229.
230.
231.
232.
233.
234.
235.
236.
237.
238.
239.
240.
241.
242.
243.
244.
245.
246.
247.
248.
249.
250.
251.
252.
253.
254.
255.
256.
257.
258.
259.
260.
261.
262.
263.
264.
265.
266.
267.
268.
269.
270.
271.
272.
273.
274.
275.
276.
277.
278.
279.
280.
281.
282.
283.
284.
285.
286.
287.
288.
289.
290.
291.
292.
293.
294.
295.
296.
297.
298.
299.
300.
301.
302.
303.
304.
305.
306.
307.
308.
309.
310.
311.
312.
313.
314.
/**
* Classe de test du repository ContratRepository
*/
@SpringBootTest
@DisplayName("Test repository ContratRepository")
@ExtendWith(SpringExtension.class)
@ActiveProfiles("test")
class ContratRepositoryTest {
public static final List<Long> CONTRAT_ID_LIST = Arrays.asList(1L, 2L);
public static final boolean CONTRAT_VERSION_ACTIF = true;
public static final String SUFFIX_MAIL = ".COM";
public static final List<SubGraphUtil> GRAPH = Arrays.asList(ContratGraph.getContratVersionGraph(),
ContratGraph.getSocPdtAvoMailGraph());
@Autowired
private ContratRepository contratRepository;
@Test
@DisplayName("test findById -> JpaRepository query")
void testFindById() {
Contrat contrat = contratRepository.findById(1L).orElse(null);
assertNotNull(contrat);
assertNoGraph(contrat);
}
@Test
@DisplayName("test findAll -> JpaRepository query")
void testFindAll() {
List<Contrat> contratList = contratRepository.findAll();
assertNoGraph(contratList);
}
@Test
@DisplayName("test findBySocieteId -> Derived query")
void testFindBySocieteId() {
List<Contrat> contratList = contratRepository.findBySocieteId(1L);
assertNoGraph(contratList);
}
@Test
@DisplayName("test findBySocieteNomStartsWith -> Derived query")
void testFindBySocieteNomStartsWith() {
List<Contrat> contratList = contratRepository.findBySocieteNomStartsWith("soc");
assertNoGraph(contratList);
}
@Test
@DisplayName("test findBySocieteIdContratIdNameResult -> Derived query projection (id,nom)")
void testFindBySocieteIdContratIdNameResult() {
List<ContratIdNameResult> contratIdNameResultList = contratRepository.findBySocieteId(1L,
ContratIdNameResult.class);
assertTrue(isNotEmpty(contratIdNameResultList));
}
@Test
@DisplayName("test findBySocieteIdContratIdResult -> Derived query projection id")
void testFindBySocieteIdContratIdResult() {
List<IdResult> idResultList = contratRepository.findBySocieteId(1L, IdResult.class);
assertTrue(isNotEmpty(idResultList));
}
@Test
@DisplayName("test findJPQL -> JPQL query with graph")
void testFindJPQLGraph() {
List<Contrat> contratList = contratRepository.findJPQLGraph();
assertTrue(isNotEmpty(contratList));
assertGraph(contratList);
}
@Test
@DisplayName("test findJPQLWhere -> JPQL query with graph and clause where")
void testFindJPQLGraphWhere() {
List<Contrat> contratList = contratRepository.findJPQLGraphWhere(CONTRAT_ID_LIST,
CONTRAT_VERSION_ACTIF,
SUFFIX_MAIL);
assertTrue(isNotEmpty(contratList));
assertGraph(contratList);
}
@Test
@DisplayName("test findJPQLWhereProjection -> JPQL query with graph, clause where and projection")
void testFindJPQLGraphWhereProjection() {
List<ContratProjectionResult> contratProjectionResultList =
contratRepository.findJPQLGraphWhereProjection(CONTRAT_ID_LIST, CONTRAT_VERSION_ACTIF,
SUFFIX_MAIL);
assertTrue(isNotEmpty(contratProjectionResultList));
}
@Test
@DisplayName("test testFindCustom -> AbstractCustomRepository query with graph")
void testFindCustomGraph() {
List<Contrat> contratList = contratRepository.findAllWithGraphAndSpecification(GRAPH, null);
assertTrue(isNotEmpty(contratList));
assertGraph(contratList);
}
@Test
@DisplayName("test findCustomWhere -> AbstractCustomRepository query with graph and clause where")
@SuppressWarnings("ConstantConditions")
void testFindCustomGraphWhere() {
Specification<Contrat> contratSpecification =
ContratSpecification.idIn(CONTRAT_ID_LIST)
.and(ContratSpecification.contratVersionActifEqual(CONTRAT_VERSION_ACTIF))
.and(ContratSpecification.adresseMailLibelleEndWithIgnoreCase(Personne_.AVOCAT,
SUFFIX_MAIL));
List<Contrat> contratList = contratRepository.findAllWithGraphAndSpecification(GRAPH,
contratSpecification);
assertTrue(isNotEmpty(contratList));
assertGraph(contratList);
}
@Test
@DisplayName("test testFindCustomWhereDynamic -> AbstractCustomRepository query graph and dynamic " +
"clause where")
void testFindCustomGraphWhereDynamic() {
// sans critere
ContratCriteria contratCriteria = ContratCriteria.builder().build();
callRepositoryAndAssert(contratCriteria);
// critere partiel
contratCriteria = ContratCriteria.builder()
.contratVersionActif(CONTRAT_VERSION_ACTIF)
.suffixAvocatMail(SUFFIX_MAIL)
.build();
callRepositoryAndAssert(contratCriteria);
// tous les criteres
contratCriteria = ContratCriteria.builder()
.contratIdList(CONTRAT_ID_LIST)
.contratVersionActif(CONTRAT_VERSION_ACTIF)
.suffixAvocatMail(SUFFIX_MAIL)
.build();
callRepositoryAndAssert(contratCriteria);
}
@Test
@DisplayName("test findAllCriteria -> Criteria query with graph")
void testFindCriteriaGraph() {
List<Contrat> contratList = contratRepository.findCriteriaGraph();
assertTrue(isNotEmpty(contratList));
assertGraph(contratList);
}
@Test
@DisplayName("test findCriteriaWhere -> Criteria query with graph and clause where")
void testFindCriteriaGraphWhere() {
List<Contrat> contratList = contratRepository.findCriteriaGraphWhere(CONTRAT_ID_LIST,
CONTRAT_VERSION_ACTIF,
SUFFIX_MAIL);
assertTrue(isNotEmpty(contratList));
assertGraph(contratList);
}
@Test
@DisplayName("test findAllCriteria -> Criteria query with graph and dynamic clause where")
void testFindCriteriaGraphWhereDynamic() {
// sans critere
ContratCriteria contratCriteria = ContratCriteria.builder().build();
List<Contrat> contratList = contratRepository.findCriteriaGraphWhereDynamic(contratCriteria);
assertTrue(isNotEmpty(contratList));
assertGraph(contratList);
// critere partiel
contratCriteria = ContratCriteria.builder()
.contratVersionActif(CONTRAT_VERSION_ACTIF)
.suffixAvocatMail(SUFFIX_MAIL)
.build();
contratList = contratRepository.findCriteriaGraphWhereDynamic(contratCriteria);
assertTrue(isNotEmpty(contratList));
assertGraph(contratList);
// tous les criteres
contratCriteria = ContratCriteria.builder()
.contratIdList(CONTRAT_ID_LIST)
.contratVersionActif(CONTRAT_VERSION_ACTIF)
.suffixAvocatMail(SUFFIX_MAIL)
.build();
contratList = contratRepository.findCriteriaGraphWhereDynamic(contratCriteria);
assertTrue(isNotEmpty(contratList));
assertGraph(contratList);
}
@Test
@DisplayName("test findProjectionCriteria -> Criteria query with graph, clause where and projection")
void testFindCriteriaGraphWhereProjection() {
List<ContratProjectionResult> contratProjectionResultList =
contratRepository.findCriteriaGraphWhereProjection(CONTRAT_ID_LIST,
CONTRAT_VERSION_ACTIF, SUFFIX_MAIL);
assertTrue(isNotEmpty(contratProjectionResultList));
}
@Test
@DisplayName("test findNativeWhereProjection -> Native query with with graph, clause where and " +
"projection")
void testFindNativeGraphWhereProjection() {
List<ContratProjectionResult> contratProjectionResultList =
contratRepository.findNativeGraphWhereProjection(CONTRAT_ID_LIST,
CONTRAT_VERSION_ACTIF, SUFFIX_MAIL);
assertTrue(isNotEmpty(contratProjectionResultList));
}
@Test
@DisplayName("test findByExemple -> QueryByExampleExecutor query")
void testFindByExemple() {
ExampleMatcher matcher = ExampleMatcher.matching().withIgnorePaths("id", "societeId");
Contrat contratExample = new Contrat();
// matcher exact
contratExample.setNom("contrat1");
matcher = matcher.withMatcher(Contrat_.NOM, GenericPropertyMatcher::exact);
List<Contrat> contratList = contratRepository.findAll(Example.of(contratExample, matcher));
assertTrue(isNotEmpty(contratList));
assertEquals(1, contratList.size());
// matcher startWith
contratExample.setNom("contrat");
matcher = matcher.withMatcher(Contrat_.NOM, GenericPropertyMatcher::startsWith);
contratList = contratRepository.findAll(Example.of(contratExample, matcher));
assertTrue(isNotEmpty(contratList));
assertEquals(3, contratList.size());
}
private void assertNoGraph(Contrat contrat) {
assertSocieteNotLoaded(contrat.getSociete());
assertContratVersionNotLoaded(contrat.getContratVersionSet());
assertContratStatutNotLoaded(contrat.getContratStatutSet());
}
private void assertNoGraph(List<Contrat> contratList) {
assertTrue(isNotEmpty(contratList));
contratList.forEach(this::assertNoGraph);
}
private void assertGraph(List<Contrat> contratList) {
contratList.forEach(contrat -> {
assertSocieteLoaded(contrat.getSociete());
assertPersonneLoaded(contrat.getSociete().getAvocat());
assertAdresseMailLoaded(contrat.getSociete().getAvocat().getAdresseMailSet());
assertPersonneLoaded(contrat.getSociete().getPresident());
assertAdresseMailNotLoaded(contrat.getSociete().getPresident().getAdresseMailSet());
assertContratVersionLoaded(contrat.getContratVersionSet());
assertContratStatutNotLoaded(contrat.getContratStatutSet());
}
);
}
private void assertSocieteNotLoaded(Societe societe) {
assertThrows(LazyInitializationException.class, () -> societe.setId(1L), "graph societe is loaded");
}
private void assertSocieteLoaded(Societe societe) {
try {
societe.setId(1L);
} catch (LazyInitializationException e) {
fail("graph societe not loaded");
}
}
private void assertPersonneLoaded(Personne personne) {
try {
personne.setId(1L);
} catch (LazyInitializationException e) {
fail("graph personne not loaded");
}
}
private void assertContratVersionNotLoaded(Set<ContratVersion> contratVersionSet) {
assertThrows(LazyInitializationException.class, contratVersionSet::iterator, "graph contratVersion " +
"is loaded");
}
private void assertContratVersionLoaded(Set<ContratVersion> contratVersionSet) {
try {
contratVersionSet.forEach(contratVersion -> contratVersion.setId(1L));
} catch (LazyInitializationException e) {
fail("graph contratVersion not loaded");
}
}
private void assertContratStatutNotLoaded(Set<ContratStatut> contratStatuts) {
assertThrows(LazyInitializationException.class, contratStatuts::iterator, "graph contratStatut is " +
"loaded");
}
private void assertAdresseMailNotLoaded(Set<AdresseMail> adresseMailSet) {
assertThrows(LazyInitializationException.class, adresseMailSet::iterator, "graph adresseMail is " +
"loaded");
}
private void assertAdresseMailLoaded(Set<AdresseMail> adresseMailSet) {
try {
adresseMailSet.forEach(adresseMail -> adresseMail.setId(1L));
} catch (LazyInitializationException e) {
fail("graph adresseMail not loaded");
}
}
private void callRepositoryAndAssert(ContratCriteria contratCriteria) {
Specification<Contrat> contratSpecification = null;
if (isNotEmpty(contratCriteria.getContratIdList())) {
contratSpecification = initOrAndSpecification(null,
ContratSpecification.idIn(contratCriteria.getContratIdList()));
}
if (contratCriteria.getContratVersionActif() != null) {
contratSpecification = initOrAndSpecification(contratSpecification,
ContratSpecification.contratVersionActifEqual(
contratCriteria.getContratVersionActif()));
}
if (isNotBlank(contratCriteria.getSuffixAvocatMail())) {
contratSpecification = initOrAndSpecification(contratSpecification,
ContratSpecification.adresseMailLibelleEndWithIgnoreCase(Personne_.AVOCAT,
contratCriteria.getSuffixAvocatMail()));
}
List<Contrat> contratList = contratRepository.findAllWithGraphAndSpecification(GRAPH,
contratSpecification);
assertTrue(isNotEmpty(contratList));
assertGraph(contratList);
}
}
VI. Conclusion▲
Si certaines requêtes ne laissent pas le choix, il y a souvent plusieurs méthodes possibles.
Arbre de décision possible :
Explication de l'arbre de décision
Dès qu'il est possible d'utiliser une méthode où il n'y a aucune implémentation (JpaRepository, Derived) autant le faire.
Comme la réutilisabilité est une notion importante, AbstractCustomRepository est prioritaire sur JPQL/Criteria.
En effet avec AbstractCustomRepository, il n'y a quasi aucun code à faire dans la couche repository, il suffit de combiner. Pour les cas avec projection DTO, difficile de donner une priorité entre JQPL et Criteria. L'implémentation JPQL est plus rapide, plus concise, mais l'API criteria est plus claire, plus maintenable.
Le choix peut aussi être guidé par le fait qu'on ne veut pas utiliser trop de techniques différentes dans un même projet, ce qui complique la maintenance, la montée de compétence éventuelle des nouveaux développeurs, etc.
Dans ce cas, on utilise Criteria, car c'est la seule méthode qui peut tout faire.
Les sources sont disponibles sur githubsources github.
VII. Remerciements▲
Je tiens à remercier Mickaël Baron, pour sa relecture technique.
Je tiens également à remercier Claude LELOUP, pour sa relecture orthographique.






