Tutoriel pour apprendre différentes façons d'écrire ses requêtes avec JPA

Christophe Agoero

La Java Persistence API (abrégée en JPA), est une interface de programmation Java permettant aux développeurs d'organiser des données relationnelles dans des applications utilisant la plateforme Java. La Java Persistence API repose essentiellement sur l'utilisation des annotations, introduites dans Java 5. Elles permettent de définir facilement des objets métier, qui pourront servir d'interface entre la base de données et l'application, dans le cadre d'un mapping objet-relationnel. Il est possible de faire du requêtage par plusieurs méthodes.

Ce tutoriel a pour but de présenter des méthodes (non exhaustif) en précisant leurs cas d'utilisations, les avantages et inconvénients. Ce tutoriel utilise Spring Data JPA qui facilite le développement d'applications JPA. L'implémentation de JPA choisie est Hibernate. Le tutoriel se focalise uniquement sur les requêtes de sélection.

Le premier chapitre aborde des notions essentielles pour comprendre JPA (graphe, projection, statique/dynamique, problèmes) permettant de définir le concept de requête efficace. Les chapitres suivants montrent l'implémentation nécessaire (entités, repository, méthodes de requêtage) d'un projet d'exemple. Le chapitre tests unitaires montre l'implémentation des appels et sert aussi à vérifier la conformité par rapport au concept de requête efficace. Dans la conclusion se trouve un arbre de décision servant à choisir la meilleure méthode selon le cas d'utilisation.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum Commentez .

- Titre : Tutoriel pour apprendre différentes façons d'écrire ses requêtes avec JPA

- Auteur : Christophe Agoero

- Parution : 18 janvier 2021

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2020 Agoero. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Notions

- I-A. Graphe

- I-B. Projection

- I-B-1. Projection DTO

- I-B-2. Projection entités

- I-C. Statique/Dynamique

- I-C-1. Requêtes statiques

- I-C-2. Requêtes dynamiques

- I-D. Problèmes JPA

- I-D-1. LazyInitializationException et N + 1

- I-D-2. Détection

- I-D-3. Résolution

- I-E. Concept de requête efficace

- II. Entités

- II-A. Modèle de données et graphe

- III. Repository

- IV. Méthodes de requêtage

- IV-A. CrudRepository query

- IV-B. Derived query

- IV-C. Example query

- IV-D. JPQL query

- IV-E. Criteria query

- IV-F. Native query

- IV-G. AbstractCustomRepository query

- V. Tests unitaires

- VI. Conclusion

- VII. Remerciements

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.

1. @Entity

2. @Table(name = "contrat")

3. public class Contrat {

4. // ...

6. @ManyToOne(fetch = FetchType.LAZY)

7. private Societe societe;

9. // ...

10. }

1. /**

2. * Classe de service du Contrat

3. */

4. @Service

5. @RequiredArgsConstructor

6. @Slf4j

7. @SuppressWarnings("squid:S4144")

8. public class ContratService {

10. private final ContratRepository contratRepository;

12. @Transactional(readOnly = true)

13. public void showNPlusOneProblem() {

14. List<Contrat> contratList = contratRepository.findAll();

15. contratList.forEach(this::printAdressMail);

16. }

18. public void showLazyInitializationException() {

19. List<Contrat> contratList = contratRepository.findAll();

20. contratList.forEach(this::printAdressMail);

21. }

23. private void printAdressMail(Contrat contrat) {

24. contrat.getSociete().getAvocat().getAdresseMailSet().forEach(

25. adresseMail -> log.info(adresseMail.toString())

26. );

27. }

29. }

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.

1. # SPRING

2. spring:

3. #JPA

4. jpa:

5. 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

1. @Entity

2. @Table(name = "table_nom")

- Pour la clé primaire

1. @Id

2. @Column(name = "colonne_nom", unique = true, nullable = false)

- Pour les clés étrangères

1. // de base on force tout à LAZY il faut le préciser, car par défaut ManyToOne et OneToOne sont en EAGER

2. @OneToOne(fetch = FetchType.LAZY)

3. @ManyToOne(fetch = FetchType.LAZY)

4. @OneToMany(fetch = FetchType.LAZY, mappedBy = "...")

5. @ManyToMany(fetch = FetchType.LAZY, mappedBy = "...")

- Pour les autres champs

1. @Basic

2. @Column(name = "colonne_nom")

- Annotation lombok sur la classe

1. @Getter

2. @Setter

3. // dans le @ToString on exclut tous les attributs LAZY

4. @ToString(exclude = {"attributLAZY_1", "attributLAZY_2", ...,"attributLAZY_N"})

Si besoin d'un tutoriel lombok, c'est ici , 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 ».

1. <?xml version="1.0" encoding="UTF-8"?>

2. <entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"

3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

4. xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm

5. http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"

6. version="2.2">

7. <persistence-unit-metadata>

8. <persistence-unit-defaults>

9. <access>FIELD</access>

10. </persistence-unit-defaults>

11. </persistence-unit-metadata>

12. </entity-mappings>

Afin d'accéder aux attributs des entités, on utilise hibernate-jpamodelgen.

1. <!-- hibernate jpamodelgen -->

2. <dependency>

3. <groupId>org.hibernate</groupId>

4. <artifactId>hibernate-jpamodelgen</artifactId>

5. </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 :

1. @Entity

2. @Table(name = "contrat")

3. @Getter

4. @Setter

5. @ToString(exclude = {"societe", "contratStatutSet", "contratVersionSet"})

6. public class Contrat {

8. @Id

9. @Column(name = "id", nullable = false)

10. private long id;

11. @Basic

12. @Column(name = "nom", nullable = false, length = 50)

13. private String nom;

15. // FK FIELD

16. @ManyToOne(fetch = FetchType.LAZY)

17. @JoinColumn(name = "fk_societe_id", nullable = false)

18. private Societe societe;

20. // JPA FIELD

21. @OneToMany(fetch = FetchType.LAZY, mappedBy = "contrat")

22. private Set<ContratStatut> contratStatutSet;

23. @OneToMany(fetch = FetchType.LAZY, mappedBy = "contrat")

24. private Set<ContratVersion> contratVersionSet;

1. @Entity

2. @Table(name = "contrat_statut")

3. @Getter

4. @Setter

5. @ToString(exclude = {"contrat"})

6. public class ContratStatut {

8. @Id

9. @Column(name = "id", nullable = false)

10. private long id;

11. @Basic

12. @Column(name = "statut", nullable = false, length = 50)

13. private String statut;

14. @Basic

15. @Column(name = "actif", nullable = false)

16. private boolean actif;

18. // FK FIELD

19. @ManyToOne(fetch = FetchType.LAZY)

20. @JoinColumn(name = "fk_contrat_id", nullable = false)

21. private Contrat contrat;

22. }

1. @Entity

2. @Table(name = "contrat_version")

3. @Getter

4. @Setter

5. @ToString(exclude = {"contrat"})

6. public class ContratVersion {

8. @Id

9. @Column(name = "id", nullable = false)

10. private long id;

11. @Basic

12. @Column(name = "nom", nullable = false, length = 50)

13. private String nom;

14. @Basic

15. @Column(name = "numero_version", nullable = false)

16. private int numeroVersion;

17. @Basic

18. @Column(name = "actif", nullable = false)

19. private boolean actif;

21. // FK FIELD

22. @ManyToOne(fetch = FetchType.LAZY)

23. @JoinColumn(name = "fk_contrat_id", nullable = false)

24. private Contrat contrat;

25. }

1. @Entity

2. @Table(name = "societe")

3. @Getter

4. @Setter

5. @ToString(exclude = {"contratSet", "avocat", "president"})

6. public class Societe {

8. @Id

9. @Column(name = "id", nullable = false)

10. private long id;

11. @Basic

12. @Column(name = "nom", nullable = false, length = 50)

13. private String nom;

14. @Basic

15. @Column(name = "numero", nullable = false, length = 5)

16. private String numero;

18. // FK FIELD

19. @ManyToOne(fetch = FetchType.LAZY)

20. @JoinColumn(name = "fk_personne_avocat_id", nullable = false)

21. private Personne avocat;

22. @ManyToOne(fetch = FetchType.LAZY)

23. @JoinColumn(name = "fk_personne_president_id", nullable = false)

24. private Personne president;

26. // JPA FIELD

27. @OneToMany(fetch = FetchType.LAZY, mappedBy = "societe")

28. private Set<Contrat> contratSet;

29. }

1. @Entity

2. @Table(name = "personne")

3. @Getter

4. @Setter

5. @ToString(exclude = {"adresseMailSet"})

6. public class Personne {

8. @Id

9. @Column(name = "id", nullable = false)

10. private long id;

11. @Basic

12. @Column(name = "nom", nullable = false, length = 50)

13. private String nom;

14. @Basic

15. @Column(name = "prenom", nullable = false, length = 50)

16. private String prenom;

17. @Basic

18. @Column(name = "avocat", nullable = false)

19. private boolean avocat;

20. @Basic

21. @Column(name = "president", nullable = false)

22. private boolean president;

24. // JPA FIELD

25. @OneToMany(fetch = FetchType.LAZY, mappedBy = "personne")

26. private Set<AdresseMail> adresseMailSet;

27. }

1. @Entity

2. @Table(name = "adresse_mail")

3. @Getter

4. @Setter

5. @ToString(exclude = {"personne"})

6. public class AdresseMail {

8. @Id

9. @Column(name = "id", nullable = false)

10. private long id;

11. @Basic

12. @Column(name = "libelle", nullable = false, length = 50)

13. private String libelle;

15. // FK FIELD

16. @ManyToOne(fetch = FetchType.LAZY)

17. @JoinColumn(name = "fk_personne_id", nullable = false)

18. private Personne personne;

19. }

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.

1. /**

2. * Interface pour les methodes non custom du repository Contrat

3. */

4. @Repository

5. public interface ContratRepository extends JpaRepository<Contrat, Long>, JpaSpecificationExecutor<Contrat>,

6. ContratCustomRepository {

7. // Déclaration des méthodes non custom

8. }

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.

1. /**

2. * Interface pour les methodes custom du repository Contrat

3. */

4. public interface ContratCustomRepository {

5. // Déclaration des méthodes custom

6. }

Une classe abstraite générique contenant deux autres méthodes est à disposition pour les repository custom.

1. /**

2. * Classe generique pour les methodes custom de repository

3. */

4. @Slf4j

5. public abstract class AbstractCustomRepository<T> {

7. /**

8. * Permet de recuperer une liste d entites en choisissant le graphe et la specification

9. *

10. * @param subGraphUtilList

11. * @param specification

12. * @return une liste d entites en choisissant le graphe

13. */

14. public List<T> findAllWithGraphAndSpecification(List<SubGraphUtil> subGraphUtilList,

15. @Nullable Specification<T> specification) {

16. // ...

17. }

19. /**

20. * Permet de recuperer une entite en choisissant le graphe et la specification

21. *

22. * @param subGraphUtilList

23. * @param specification

24. * @return une entite en choisissant le graphe

25. */

26. public Optional<T> findOneWithGraphAndSpecification(List<SubGraphUtil> subGraphUtilList,

27. @Nullable Specification<T> specification) {

28. //...

29. }

30. }

SubGraphUtil est une classe utilitaire servant à créer les graphes.

1. /**

2. * Classe utilitaire pour creer les graphes

3. */

4. @Getter

5. @Setter

6. public final class SubGraphUtil {

8. private String name;

9. private List<SubGraphUtil> subGraphUtilList;

11. public SubGraphUtil(String name) {

12. this.name = name;

13. }

14. }

La classe d'implémentation du repository custom étend la classe abstraite générique pour bénéficier des méthodes.

1. /**

2. * Classe d implementation des methodes custom du repository contrat

3. */

4. @SuppressWarnings({"unused","squid:S1192"})

5. public class ContratCustomRepositoryImpl extends AbstractCustomRepository<Contrat>

6. implements ContratCustomRepository {

7. // Implémentation des méthodes custom

8. }

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 :

1. @Data

2. @Builder

3. public class ContratCriteria {

5. // contrat

6. private List<Long> contratIdList;

7. // contratVersion

8. private Boolean contratVersionActif;

9. // societe

10. // adresseMail

11. private String suffixAvocatMail;

12. }

L'objet commun ContratProjectionResult pour la projection DTO :

1. @Value

2. public class ContratProjectionResult {

4. // contrat

5. long contratId;

6. String contratNom;

7. // contratVersion

8. long contratVersionId;

9. int contratVersionNumero;

10. // societe

11. long societeId;

12. String societeNom;

13. // personne

14. String avocatNom;

15. String presidentNom;

16. // adresseMail

17. String avocatMail;

18. }

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.

1. List<Contrat> findBySocieteId(long societeId);

3. List<Contrat> findBySocieteNomStartsWith(String prefixSociete);

5. <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.

1. @Value

2. public class ContratIdNameResult {

4. long id;

5. String nom;

6. }

1. @Value

2. public class IdResult {

4. long id;

5. }

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.

1. List<Contrat> findJPQLGraph();

3. List<Contrat> findJPQLGraphWhere(List<Long> contratIdList, boolean contratVersionActif,

4. String suffixMail);

6. List<ContratProjectionResult> findJPQLGraphWhereProjection(List<Long> contratIdList,

7. boolean contratVersionActif,

8. String suffixMail);

Implémentation dans ContratCustomRepositoryImpl

1. @Override

2. public List<Contrat> findJPQLGraph() {

3. TypedQuery<Contrat> typedQuery = entityManager.createQuery("SELECT DISTINCT c FROM Contrat c "

4. + "JOIN FETCH c.contratVersionSet cv "

5. + "JOIN FETCH c.societe s "

6. + "JOIN FETCH s.avocat a "

7. + "JOIN FETCH a.adresseMailSet am "

8. + "JOIN FETCH s.president p ",

9. Contrat.class

10. );

11. return typedQuery.getResultList();

12. }

14. @Override

15. public List<Contrat> findJPQLGraphWhere(List<Long> contratIdList, boolean contratVersionActif,

16. String suffixMail) {

17. TypedQuery<Contrat> typedQuery = entityManager.createQuery("SELECT DISTINCT c FROM Contrat c "

18. + "JOIN FETCH c.contratVersionSet cv "

19. + "JOIN FETCH c.societe s "

20. + "JOIN FETCH s.avocat a "

21. + "JOIN FETCH a.adresseMailSet am "

22. + "JOIN FETCH s.president p "

23. + "WHERE c.id IN (:contratIdList) "

24. + "AND cv.actif = :contratVersionActif "

25. + "AND UPPER(am.libelle)  LIKE :suffixMail ",

26. Contrat.class

27. );

28. typedQuery.setParameter("contratIdList", contratIdList);

29. typedQuery.setParameter("contratVersionActif", contratVersionActif);

30. typedQuery.setParameter("suffixMail", "%" + suffixMail.toUpperCase());

31. return typedQuery.getResultList();

32. }

34. @Override

35. public List<ContratProjectionResult> findJPQLGraphWhereProjection(List<Long> contratIdList,

36. boolean contratVersionActif,

37. String suffixMail) {

38. TypedQuery<ContratProjectionResult> typedQuery = entityManager.createQuery("SELECT NEW fr.agoero" +

39. ".result" +

40. ".ContratProjectionResult("

41. + "c.id,"

42. + "c.nom,"

43. + "cv.id,"

44. + "cv.numeroVersion,"

45. + "s.id,"

46. + "s.nom,"

47. + "a.nom,"

48. + "p.nom,"

49. + "am.libelle)"

50. + "FROM Contrat c "

51. + "JOIN  c.contratVersionSet cv "

52. + "JOIN  c.societe s "

53. + "JOIN  s.avocat a "

54. + "JOIN  a.adresseMailSet am "

55. + "JOIN  s.president p "

56. + "WHERE c.id IN (:contratIdList) "

57. + "AND cv.actif = :contratVersionActif "

58. + "AND UPPER(am.libelle)  LIKE :suffixMail ",

59. ContratProjectionResult.class

60. );

61. typedQuery.setParameter("contratIdList", contratIdList);

62. typedQuery.setParameter("contratVersionActif", contratVersionActif);

63. typedQuery.setParameter("suffixMail", "%" + suffixMail.toUpperCase());

64. return typedQuery.getResultList();

65. }

Une autre implémentation moins verbeuse est possible en utilisant @Query.

1. /**

2. * Interface pour les methodes non custom du repository Contrat

3. */

4. @Repository

5. public interface ContratRepository extends JpaRepository<Contrat, Long>, JpaSpecificationExecutor<Contrat>,

6. ContratCustomRepository {

8. @Query("SELECT DISTINCT c FROM Contrat c "

9. + "JOIN FETCH c.contratVersionSet cv "

10. + "JOIN FETCH c.societe s "

11. + "JOIN FETCH s.avocat a "

12. + "JOIN FETCH a.adresseMailSet am "

13. + "JOIN FETCH s.president p ")

14. List<Contrat> findJPQLGraph();

15. }

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.

1. List<Contrat> findCriteriaGraph();

3. List<Contrat> findCriteriaGraphWhere(List<Long> contratIdList, boolean contratVersionActif,

4. String suffixMail);

6. List<ContratProjectionResult> findCriteriaGraphWhereProjection(List<Long> contratIdList,

7. boolean contratVersionActif,

8. String suffixMail);

10. List<Contrat> findCriteriaGraphWhereDynamic(ContratCriteria contratCriteria);

Implémentation dans ContratCustomRepositoryImpl

1. @Override

2. public List<Contrat> findCriteriaGraph() {

3. CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();

4. CriteriaQuery<Contrat> criteriaQuery = criteriaBuilder.createQuery(Contrat.class);

5. Root<Contrat> contratRoot = criteriaQuery.from(Contrat.class);

6. contratRoot.fetch(Contrat_.CONTRAT_VERSION_SET);

7. Fetch<Contrat, Societe> societeFetch = contratRoot.fetch(Contrat_.SOCIETE);

8. Fetch<Societe, Personne> avocatFetch = societeFetch.fetch(Societe_.AVOCAT);

9. avocatFetch.fetch(Personne_.ADRESSE_MAIL_SET);

10. societeFetch.fetch(Societe_.PRESIDENT);

11. criteriaQuery.distinct(true);

12. return entityManager.createQuery(criteriaQuery).getResultList();

13. }

15. @Override

16. @SuppressWarnings("unchecked")

17. public List<Contrat> findCriteriaGraphWhere(List<Long> contratIdList, boolean contratVersionActif,

18. String suffixMail) {

19. CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();

20. CriteriaQuery<Contrat> criteriaQuery = criteriaBuilder.createQuery(Contrat.class);

21. criteriaQuery.distinct(true);

22. Root<Contrat> contratRoot = criteriaQuery.from(Contrat.class);

23. Fetch<Contrat, ContratVersion> contratVersionFetch = contratRoot.fetch(Contrat_.CONTRAT_VERSION_SET);

24. Join<Contrat, ContratVersion> contratVersionJoin =

25. (Join<Contrat, ContratVersion>) contratVersionFetch;

26. Fetch<Contrat, Societe> societeFetch = contratRoot.fetch(Contrat_.SOCIETE);

27. Fetch<Societe, Personne> avocatFetch = societeFetch.fetch(Societe_.AVOCAT);

28. Fetch<Personne, AdresseMail> adresseMailFetch = avocatFetch.fetch(Personne_.ADRESSE_MAIL_SET);

29. Join<Personne, AdresseMail> adresseMailJoin = (Join<Personne, AdresseMail>) adresseMailFetch;

30. societeFetch.fetch(Societe_.PRESIDENT);

31. // predicate

32. Predicate contratIdIn = contratRoot.get(Contrat_.ID).in(contratIdList);

33. Predicate contratVersionActifEqual =

34. criteriaBuilder.equal(contratVersionJoin.get(ContratVersion_.ACTIF),

35. contratVersionActif);

36. Predicate adresseMailEndWithIgnoreCase =

37. criteriaBuilder.like(criteriaBuilder.upper(adresseMailJoin.get(AdresseMail_.LIBELLE)),

38. "%" + suffixMail.toUpperCase());

39. criteriaQuery.where(criteriaBuilder.and(contratIdIn, contratVersionActifEqual,

40. adresseMailEndWithIgnoreCase));

41. return entityManager.createQuery(criteriaQuery).getResultList();

42. }

44. @Override

45. public List<ContratProjectionResult> findCriteriaGraphWhereProjection(List<Long> contratIdList,

46. boolean contratVersionActif,

47. String suffixMail) {

48. CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();

49. CriteriaQuery<ContratProjectionResult> criteriaQuery =

50. criteriaBuilder.createQuery(ContratProjectionResult.class);

51. Root<Contrat> contratRoot = criteriaQuery.from(Contrat.class);

52. Join<Contrat, ContratVersion> contratVersionJoin = contratRoot.join(Contrat_.CONTRAT_VERSION_SET);

53. Join<Contrat, Societe> societeJoin = contratRoot.join(Contrat_.SOCIETE);

54. Join<Societe, Personne> avocatJoin = societeJoin.join(Societe_.AVOCAT);

55. Join<Personne, AdresseMail> adresseMailJoin = avocatJoin.join(Personne_.ADRESSE_MAIL_SET);

56. Join<Societe, Personne> presidentJoin = societeJoin.join(Societe_.PRESIDENT);

57. criteriaQuery.select(criteriaBuilder.construct(ContratProjectionResult.class,

58. contratRoot.get(Contrat_.ID),

59. contratRoot.get(Contrat_.NOM),

60. contratVersionJoin.get(ContratVersion_.ID),

61. contratVersionJoin.get(ContratVersion_.NUMERO_VERSION),

62. societeJoin.get(Societe_.ID),

63. societeJoin.get(Societe_.NOM),

64. avocatJoin.get(Personne_.NOM),

65. presidentJoin.get(Personne_.NOM),

66. adresseMailJoin.get(AdresseMail_.LIBELLE)

67. )

68. );

69. // predicate

70. Predicate contratIdIn = contratRoot.get(Contrat_.ID).in(contratIdList);

71. Predicate contratVersionActifEqual =

72. criteriaBuilder.equal(contratVersionJoin.get(ContratVersion_.ACTIF),

73. contratVersionActif);

74. Predicate adresseMailEndWithIgnoreCase =

75. criteriaBuilder.like(criteriaBuilder.upper(adresseMailJoin.get(AdresseMail_.LIBELLE)),

76. "%" + suffixMail.toUpperCase());

78. criteriaQuery.where(criteriaBuilder.and(contratIdIn, contratVersionActifEqual,

79. adresseMailEndWithIgnoreCase));

80. return entityManager.createQuery(criteriaQuery).getResultList();

81. }

83. @SuppressWarnings("unchecked")

84. @Override

85. public List<Contrat> findCriteriaGraphWhereDynamic(ContratCriteria contratCriteria) {

86. List<Predicate> predicateList = new ArrayList<>();

87. CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();

88. CriteriaQuery<Contrat> criteriaQuery = criteriaBuilder.createQuery(Contrat.class);

89. criteriaQuery.distinct(true);

90. Root<Contrat> contratRoot = criteriaQuery.from(Contrat.class);

91. Fetch<Contrat, ContratVersion> contratVersionFetch = contratRoot.fetch(Contrat_.CONTRAT_VERSION_SET);

92. Fetch<Contrat, Societe> societeFetch = contratRoot.fetch(Contrat_.SOCIETE);

93. Fetch<Societe, Personne> avocatFetch = societeFetch.fetch(Societe_.AVOCAT);

94. Fetch<Personne, AdresseMail> adresseMailFetch = avocatFetch.fetch(Personne_.ADRESSE_MAIL_SET);

95. societeFetch.fetch(Societe_.PRESIDENT);

96. // predicate

97. if (isNotEmpty(contratCriteria.getContratIdList())) {

98. Predicate contratIdIn = contratRoot.get(Contrat_.ID).in(contratCriteria.getContratIdList());

99. predicateList.add(contratIdIn);

100. }

101. if (contratCriteria.getContratVersionActif() != null) {

102. Join<Contrat, ContratVersion> contratVersionJoin =

103. (Join<Contrat, ContratVersion>) contratVersionFetch;

104. Predicate contratVersionActifEqual =

105. criteriaBuilder.equal(contratVersionJoin.get(ContratVersion_.ACTIF),

106. contratCriteria.getContratVersionActif());

107. predicateList.add(contratVersionActifEqual);

108. }

109. if (isNotBlank(contratCriteria.getSuffixAvocatMail())) {

110. Join<Personne, AdresseMail> adresseMailJoin = (Join<Personne, AdresseMail>) adresseMailFetch;

111. Predicate adresseMailEndWithIgnoreCase =

112. criteriaBuilder.like(criteriaBuilder.upper(adresseMailJoin.get(AdresseMail_.LIBELLE)),

113. "%" + contratCriteria.getSuffixAvocatMail().toUpperCase());

114. predicateList.add(adresseMailEndWithIgnoreCase);

115. }

116. criteriaQuery.where(predicateList.toArray(new Predicate[]{}));

117. return entityManager.createQuery(criteriaQuery).getResultList();

118. }

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.

1. List<ContratProjectionResult> findNativeGraphWhereProjection(List<Long> contratIdList,

2. boolean contratVersionActif,

3. String suffixMail);

Implémentation dans ContratCustomRepositoryImpl

1. @Override

2. @SuppressWarnings("unchecked")

3. public List<ContratProjectionResult> findNativeGraphWhereProjection(List<Long> contratIdList,

4. boolean contratVersionActif,

5. String suffixMail) {

6. Query query = entityManager.createNativeQuery("SELECT "

7. + "c.id AS contratId, "

8. + "c.nom AS contratNom, "

9. + "cv.id AS contratVersionId, "

10. + "cv.numero_version AS contratVersionNumero, "

11. + "s.id AS societeId, "

12. + "s.nom AS societeNom, "

13. + "a.nom AS avocatNom, "

14. + "p.nom AS presidentNom, "

15. + "am.libelle AS avocatMail "

16. // from

17. + "FROM contrat c "

18. + "JOIN contrat_version cv ON c.id = cv.fk_contrat_id "

19. + "JOIN societe s ON c.fk_societe_id = s.id  "

20. + "JOIN personne p ON s.fk_personne_president_id = p.id "

21. + "JOIN personne a ON s.fk_personne_avocat_id = a.id "

22. + "JOIN adresse_mail am ON a.id = am.fk_personne_id "

23. // where

24. + "WHERE c.id IN (:contratIdList) "

25. + "AND cv.actif = :contratVersionActif "

26. + "AND UPPER(am.libelle)  LIKE :suffixMail",

27. "ContratProjectionMapping");

28. query.setParameter("contratIdList", contratIdList);

29. query.setParameter("contratVersionActif", contratVersionActif);

30. query.setParameter("suffixMail", "%" + suffixMail.toUpperCase());

31. return query.getResultList();

32. }

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 :

1. @MappedSuperclass

2. @SqlResultSetMapping(

3. name = "ContratProjectionMapping",

4. classes = @ConstructorResult(

5. targetClass = ContratProjectionResult.class,

6. columns = {

7. @ColumnResult(name = "contratId", type = Long.class),

8. @ColumnResult(name = "contratNom", type = String.class),

9. @ColumnResult(name = "contratVersionId", type = Long.class),

10. @ColumnResult(name = "contratVersionNumero", type = Integer.class),

11. @ColumnResult(name = "societeId", type = Long.class),

12. @ColumnResult(name = "societeNom", type = String.class),

13. @ColumnResult(name = "avocatNom", type = String.class),

14. @ColumnResult(name = "presidentNom", type = String.class),

15. @ColumnResult(name = "avocatMail", type = String.class)

16. }

17. )

18. )

19. public class ContratProjectionMapping {

21. }

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 :

1. Caused by: org.hibernate.tool.schema.spi.SchemaManagementException:

2. 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) :

1. Hibernate:

3. create table contrat_projection_mapping (

4. id int8 not null,

5. primary key (id)

6. )

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.

1. List<Contrat> findAllWithGraphAndSpecification(List<SubGraphUtil> subGraphUtilList,

2. Specification<Contrat> specification);

Implémentation partie graphe :
pour les graphes, on se sert de l'utilitaire SubGraphUtil pour chaque entité.

1. /**

2. * Classe de graphes pour Contrat

3. */

4. @NoArgsConstructor(access = AccessLevel.PRIVATE)

5. public final class ContratGraph {

7. /**

8. * Permet de construire le sous-graphe ContratVersion

9. *

10. * @return le sous-graphe ContratVersion

11. */

12. public static SubGraphUtil getContratVersionGraph() {

13. return new SubGraphUtil(Contrat_.CONTRAT_VERSION_SET);

14. }

16. /**

17. * Permet de construire le sous-graphe Societe

18. *

19. * @return le sous-graphe Societe

20. */

21. public static SubGraphUtil getSocieteGraph() {

22. return new SubGraphUtil(Contrat_.SOCIETE);

23. }

25. /**

26. * Permet de construire le sous-graphe Societe President Avocat AdresseMail

27. *

28. * @return le sousgraphe Avocat

29. */

30. public static SubGraphUtil getSocPdtAvoMailGraph() {

31. SubGraphUtil subGraphSocPdtAvoMail = getSocieteGraph();

32. subGraphSocPdtAvoMail.setSubGraphUtilList(Arrays.asList(SocieteGraph.getPresidentGraph(),

33. SocieteGraph.getAvocatMailGraph()));

34. return subGraphSocPdtAvoMail;

35. }

36. }

1. /**

2. * Classe de graphes pour Societe

3. */

4. @NoArgsConstructor(access = AccessLevel.PRIVATE)

5. public final class SocieteGraph {

8. /**

9. * Permet de construire le sous-graphe Avocat

10. *

11. * @return le sous-graphe Avocat

12. */

13. public static SubGraphUtil getAvocatGraph() {

14. return new SubGraphUtil(Societe_.AVOCAT);

15. }

17. /**

18. * Permet de construire le sous-graphe President

19. *

20. * @return le sous-graphe President

21. */

22. public static SubGraphUtil getPresidentGraph() {

23. return new SubGraphUtil(Societe_.PRESIDENT);

24. }

26. /**

27. * Permet de construire le sous-graphe AvocatMail

28. *

29. * @return le sous-graphe AvocatMail

30. */

31. public static SubGraphUtil getAvocatMailGraph() {

32. SubGraphUtil subGraphAvocat = SocieteGraph.getAvocatGraph();

33. SubGraphUtil subGraphAdresseMail = PersonGraph.getAdresseEmailGraph();

34. subGraphAvocat.setSubGraphUtilList(Collections.singletonList(subGraphAdresseMail));

35. return subGraphAvocat;

36. }

37. }

1. /**

2. * Classe de graphes pour Personne

3. */

4. @NoArgsConstructor(access = AccessLevel.PRIVATE)

5. public final class PersonGraph {

7. /**

8. * Permet de construire le sous-graphe AdresseEmail

9. *

10. * @return le sousgraphe AdresseEmail

11. */

12. public static SubGraphUtil getAdresseEmailGraph() {

13. return new SubGraphUtil(Personne_.ADRESSE_MAIL_SET);

14. }

15. }

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.

1. @SuppressWarnings("rawtypes")

2. @NoArgsConstructor(access = AccessLevel.PRIVATE)

3. public final class SpecificationUtil {

5. public static <X> Join getJoin(final Root<X> root, final String attributeName) {

6. final Optional<Join<X, ?>> join = root.getJoins()

7. .stream()

8. .filter(equalAttributeNamePredicate(attributeName))

9. .findFirst();

10. return join.orElseGet(() -> root.join(attributeName));

11. }

13. public static <X, Z> Join getJoin(final Join<X, Z> initialJoin, final String attributeName) {

14. final Optional<Join<Z, ?>> join = initialJoin.getJoins()

15. .stream()

16. .filter(equalAttributeNamePredicate(attributeName))

17. .findFirst();

18. return join.orElseGet(() -> initialJoin.join(attributeName));

19. }

21. public static <X> Specification<X> initOrAndSpecification(Specification<X> specification,

22. Specification<X> specificationToAnd) {

23. return specification == null ? specificationToAnd : specification.and(specificationToAnd);

24. }

26. private static <T> Predicate<Join<T, ?>> equalAttributeNamePredicate(String attributeName) {

27. return join -> join.getAttribute()

28. .getName()

29. .equals(attributeName);

30. }

31. }

La méthode initOrAndSpecification sert pour les requêtes dynamiques, pas pour la déclaration des prédicats.
Classes de spécification :

1. /**

2. * Classe de specification pour Contrat

3. */

4. @NoArgsConstructor(access = AccessLevel.PRIVATE)

5. @SuppressWarnings({"rawtypes", "unchecked"})

6. public final class ContratSpecification {

8. public static Specification<Contrat> idIn(final List<Long> contratIdList) {

9. return (root, query, criteriaBuilder) -> getIdIn(contratIdList, root, criteriaBuilder);

10. }

12. static Predicate getIdIn(final List<Long> contratIdList, final From from,

13. final CriteriaBuilder criteriaBuilder) {

14. return criteriaBuilder.in(from.get(Contrat_.ID)).value(contratIdList);

15. }

17. public static Specification<Contrat> contratVersionActifEqual(final boolean active) {

18. return (root, query, criteriaBuilder) -> {

19. final Join<Contrat, ContratVersion> contratVersionJoin = getJoin(root,

20. Contrat_.CONTRAT_VERSION_SET);

21. return ContratVersionSpecification.getActifEqual(active, contratVersionJoin, criteriaBuilder);

22. };

23. }

25. public static Specification<Contrat> adresseMailLibelleEndWithIgnoreCase(final String rolePersonne,

26. final String suffixMail) {

27. return (root, query, criteriaBuilder) -> {

28. final Join<Contrat, Societe> societeJoin = getJoin(root, Contrat_.SOCIETE);

29. final Join<Societe, Personne> personneJoin = getJoin(societeJoin, rolePersonne);

30. final Join<Personne, AdresseMail> adresseMAilJoin = getJoin(personneJoin,

31. Personne_.ADRESSE_MAIL_SET);

32. return AdresseMailSpecification.getLibelleEndWithIgnoreCase(suffixMail, adresseMAilJoin,

33. criteriaBuilder);

34. };

35. }

36. }

1. /**

2. * Classe de specification pour ContratVersion

3. */

4. @NoArgsConstructor(access = AccessLevel.PRIVATE)

5. @SuppressWarnings("rawtypes")

6. public final class ContratVersionSpecification {

8. public static Specification<ContratVersion> actifEqual(final boolean active) {

9. return (root, query, criteriaBuilder) ->

10. getActifEqual(active, root, criteriaBuilder);

11. }

13. static Predicate getActifEqual(final boolean actif, final From from,

14. final CriteriaBuilder criteriaBuilder) {

15. return criteriaBuilder.equal(from.get(ContratVersion_.ACTIF), actif);

16. }

17. }

1. /**

2. * Classe de specification pour AdresseMail

3. */

4. @NoArgsConstructor(access = AccessLevel.PRIVATE)

5. @SuppressWarnings({"rawtypes", "unchecked"})

6. public final class AdresseMailSpecification {

8. public static Specification<Contrat> libelleEndWithIgnoreCase(final String suffixMail) {

9. return (root, query, criteriaBuilder) -> getLibelleEndWithIgnoreCase(suffixMail, root,

10. criteriaBuilder);

11. }

13. static Predicate getLibelleEndWithIgnoreCase(final String suffixMail, final From from,

14. final CriteriaBuilder criteriaBuilder) {

15. return criteriaBuilder.like(criteriaBuilder.upper(from.get(AdresseMail_.LIBELLE)),

16. "%" + suffixMail.toUpperCase());

17. }

18. }

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é :

1. private void assertSocieteNotLoaded(Societe societe) {

2. assertThrows(LazyInitializationException.class, () -> societe.setId(1L), "graph societe is loaded");

3. }

5. private void assertSocieteLoaded(Societe societe) {

6. try {

7. societe.setId(1L);

8. } catch (LazyInitializationException e) {

9. fail("graph societe not loaded");

10. }

11. }

Assert pour les requêtes récupérant le graphe :

1. private void assertGraph(List<Contrat> contratList) {

2. contratList.forEach(contrat -> {

3. assertSocieteLoaded(contrat.getSociete());

4. assertPersonneLoaded(contrat.getSociete().getAvocat());

5. assertAdresseMailLoaded(contrat.getSociete().getAvocat().getAdresseMailSet());

6. assertPersonneLoaded(contrat.getSociete().getPresident());

7. assertAdresseMailNotLoaded(contrat.getSociete().getPresident().getAdresseMailSet());

8. assertContratVersionLoaded(contrat.getContratVersionSet());

9. assertContratStatutNotLoaded(contrat.getContratStatutSet());

10. }

11. );

12. }

Les tests unitaires utilisent une base H2 en mémoire. Hibernate génère le MPD à partir des entités (ddl-auto: create).
Configuration :

1. # SPRING

2. spring:

3. # DATASOURCE

4. datasource:

5. url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE

6. driver-class-name: org.h2.Driver

7. #JPA

8. jpa:

9. database: h2

10. hibernate:

11. use-new-id-generator-mappings: true

12. ddl-auto: create

13. open-in-view: false

14. properties:

15. hibernate:

16. format_sql: true

17. use_sql_comments: false

18. dialect: org.hibernate.dialect.H2Dialect

19. 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 :

1. -- personne

2. INSERT INTO personne (id, nom, prenom, avocat, president)

3. VALUES (1, 'avocat1nom', 'avocat1prenom', true, false);

5. INSERT INTO personne (id, nom, prenom, avocat, president)

6. VALUES (2, 'president1nom', 'president1prenom', false, true);

8. INSERT INTO personne (id, nom, prenom, avocat, president)

9. VALUES (3, 'avocat2nom', 'avocat2prenom', true, false);

11. INSERT INTO personne (id, nom, prenom, avocat, president)

12. VALUES (4, 'president2nom', 'president2prenom', false, true);

14. -- adresse_mail

15. INSERT INTO adresse_mail (id, libelle, fk_personne_id)

16. VALUES (1, 'avocat1@societe1.fr', 1);

18. INSERT INTO adresse_mail (id, libelle, fk_personne_id)

19. VALUES (2, 'avocat1@soc1.com', 1);

21. INSERT INTO adresse_mail (id, libelle, fk_personne_id)

22. VALUES (3, 'avocat1@societe1.com', 1);

24. INSERT INTO adresse_mail (id, libelle, fk_personne_id)

25. VALUES (4, 'president1@societe1.fr', 2);

27. INSERT INTO adresse_mail (id, libelle, fk_personne_id)

28. VALUES (5, 'president1@soc1.com', 2);

30. INSERT INTO adresse_mail (id, libelle, fk_personne_id)

31. VALUES (6, 'president2@soc2.com', 4);

33. INSERT INTO adresse_mail (id, libelle, fk_personne_id)

34. VALUES (7, 'avocat2@societe2.com', 3);

36. -- societe

37. INSERT INTO societe (id, nom, numero, fk_personne_avocat_id, fk_personne_president_id)

38. VALUES (1, 'societe1', '1111', 1, 2);

40. INSERT INTO societe (id, nom, numero, fk_personne_avocat_id, fk_personne_president_id)

41. VALUES (2, 'societe2', '2222', 3, 4);

43. -- contrat

44. INSERT INTO contrat (id, nom, fk_societe_id)

45. VALUES (1, 'contrat1', 1);

47. INSERT INTO contrat (id, nom, fk_societe_id)

48. VALUES (2, 'contrat2', 1);

50. INSERT INTO contrat (id, nom, fk_societe_id)

51. VALUES (3, 'contrat3', 2);

53. -- contrat_statut

54. INSERT INTO contrat_statut (id, statut, actif, fk_contrat_id)

55. VALUES (1, 'en_cours', false, 1);

57. INSERT INTO contrat_statut (id, statut, actif, fk_contrat_id)

58. VALUES (2, 'annule', true, 1);

60. INSERT INTO contrat_statut (id, statut, actif, fk_contrat_id)

61. VALUES (3, 'en_cours', false, 2);

63. INSERT INTO contrat_statut (id, statut, actif, fk_contrat_id)

64. VALUES (4, 'ternime', true, 2);

66. INSERT INTO contrat_statut (id, statut, actif, fk_contrat_id)

67. VALUES (5, 'ternime', true, 3);

69. -- contrat_version

70. INSERT INTO contrat_version (id, nom, numero_version, actif, fk_contrat_id)

71. VALUES (1, 'version1', 1, false, 1);

73. INSERT INTO contrat_version (id, nom, numero_version, actif, fk_contrat_id)

74. VALUES (2, 'version2', 2, true, 1);

76. INSERT INTO contrat_version (id, nom, numero_version, actif, fk_contrat_id)

77. VALUES (3, 'version1', 1, true, 2);

79. INSERT INTO contrat_version (id, nom, numero_version, actif, fk_contrat_id)

80. VALUES (4, 'version1', 1, true, 3);

Classe de tests unitaires du repository :

1. /**

2. * Classe de test du repository ContratRepository

3. */

4. @SpringBootTest

5. @DisplayName("Test repository ContratRepository")

6. @ExtendWith(SpringExtension.class)

7. @ActiveProfiles("test")

8. class ContratRepositoryTest {

10. public static final List<Long> CONTRAT_ID_LIST = Arrays.asList(1L, 2L);

11. public static final boolean CONTRAT_VERSION_ACTIF = true;

12. public static final String SUFFIX_MAIL = ".COM";

13. public static final List<SubGraphUtil> GRAPH = Arrays.asList(ContratGraph.getContratVersionGraph(),

14. ContratGraph.getSocPdtAvoMailGraph());

16. @Autowired

17. private ContratRepository contratRepository;

19. @Test

20. @DisplayName("test findById -> JpaRepository query")

21. void testFindById() {

22. Contrat contrat = contratRepository.findById(1L).orElse(null);

23. assertNotNull(contrat);

24. assertNoGraph(contrat);

25. }

27. @Test

28. @DisplayName("test findAll -> JpaRepository query")

29. void testFindAll() {

30. List<Contrat> contratList = contratRepository.findAll();

31. assertNoGraph(contratList);

32. }

34. @Test

35. @DisplayName("test findBySocieteId -> Derived query")

36. void testFindBySocieteId() {

37. List<Contrat> contratList = contratRepository.findBySocieteId(1L);

38. assertNoGraph(contratList);

39. }

41. @Test

42. @DisplayName("test findBySocieteNomStartsWith -> Derived query")

43. void testFindBySocieteNomStartsWith() {

44. List<Contrat> contratList = contratRepository.findBySocieteNomStartsWith("soc");

45. assertNoGraph(contratList);

46. }

48. @Test

49. @DisplayName("test findBySocieteIdContratIdNameResult -> Derived query projection (id,nom)")

50. void testFindBySocieteIdContratIdNameResult() {

51. List<ContratIdNameResult> contratIdNameResultList = contratRepository.findBySocieteId(1L,

52. ContratIdNameResult.class);

53. assertTrue(isNotEmpty(contratIdNameResultList));

54. }

56. @Test

57. @DisplayName("test findBySocieteIdContratIdResult -> Derived query projection id")

58. void testFindBySocieteIdContratIdResult() {

59. List<IdResult> idResultList = contratRepository.findBySocieteId(1L, IdResult.class);

60. assertTrue(isNotEmpty(idResultList));

61. }

63. @Test

64. @DisplayName("test findJPQL -> JPQL query with graph")

65. void testFindJPQLGraph() {

66. List<Contrat> contratList = contratRepository.findJPQLGraph();

67. assertTrue(isNotEmpty(contratList));

68. assertGraph(contratList);

69. }

71. @Test

72. @DisplayName("test findJPQLWhere -> JPQL query with graph and clause where")

73. void testFindJPQLGraphWhere() {

74. List<Contrat> contratList = contratRepository.findJPQLGraphWhere(CONTRAT_ID_LIST,

75. CONTRAT_VERSION_ACTIF,

76. SUFFIX_MAIL);

77. assertTrue(isNotEmpty(contratList));

78. assertGraph(contratList);

79. }

81. @Test

82. @DisplayName("test findJPQLWhereProjection -> JPQL query with graph, clause where and projection")

83. void testFindJPQLGraphWhereProjection() {

84. List<ContratProjectionResult> contratProjectionResultList =

85. contratRepository.findJPQLGraphWhereProjection(CONTRAT_ID_LIST, CONTRAT_VERSION_ACTIF,

86. SUFFIX_MAIL);

87. assertTrue(isNotEmpty(contratProjectionResultList));

88. }

90. @Test

91. @DisplayName("test testFindCustom -> AbstractCustomRepository query with graph")

92. void testFindCustomGraph() {

93. List<Contrat> contratList = contratRepository.findAllWithGraphAndSpecification(GRAPH, null);

94. assertTrue(isNotEmpty(contratList));

95. assertGraph(contratList);

96. }

98. @Test

99. @DisplayName("test findCustomWhere -> AbstractCustomRepository query with graph and clause where")

100. @SuppressWarnings("ConstantConditions")

101. void testFindCustomGraphWhere() {

102. Specification<Contrat> contratSpecification =

103. ContratSpecification.idIn(CONTRAT_ID_LIST)

104. .and(ContratSpecification.contratVersionActifEqual(CONTRAT_VERSION_ACTIF))

105. .and(ContratSpecification.adresseMailLibelleEndWithIgnoreCase(Personne_.AVOCAT,

106. SUFFIX_MAIL));

107. List<Contrat> contratList = contratRepository.findAllWithGraphAndSpecification(GRAPH,

108. contratSpecification);

109. assertTrue(isNotEmpty(contratList));

110. assertGraph(contratList);

111. }

113. @Test

114. @DisplayName("test testFindCustomWhereDynamic -> AbstractCustomRepository query graph and dynamic " +

115. "clause where")

116. void testFindCustomGraphWhereDynamic() {

117. // sans critere

118. ContratCriteria contratCriteria = ContratCriteria.builder().build();

119. callRepositoryAndAssert(contratCriteria);

120. // critere partiel

121. contratCriteria = ContratCriteria.builder()

122. .contratVersionActif(CONTRAT_VERSION_ACTIF)

123. .suffixAvocatMail(SUFFIX_MAIL)

124. .build();

125. callRepositoryAndAssert(contratCriteria);

126. // tous les criteres

127. contratCriteria = ContratCriteria.builder()

128. .contratIdList(CONTRAT_ID_LIST)

129. .contratVersionActif(CONTRAT_VERSION_ACTIF)

130. .suffixAvocatMail(SUFFIX_MAIL)

131. .build();

132. callRepositoryAndAssert(contratCriteria);

133. }

135. @Test

136. @DisplayName("test findAllCriteria -> Criteria query with graph")

137. void testFindCriteriaGraph() {

138. List<Contrat> contratList = contratRepository.findCriteriaGraph();

139. assertTrue(isNotEmpty(contratList));

140. assertGraph(contratList);

141. }

143. @Test

144. @DisplayName("test findCriteriaWhere -> Criteria query with graph and clause where")

145. void testFindCriteriaGraphWhere() {

146. List<Contrat> contratList = contratRepository.findCriteriaGraphWhere(CONTRAT_ID_LIST,

147. CONTRAT_VERSION_ACTIF,

148. SUFFIX_MAIL);

149. assertTrue(isNotEmpty(contratList));

150. assertGraph(contratList);

151. }

153. @Test

154. @DisplayName("test findAllCriteria -> Criteria query with graph and dynamic clause where")

155. void testFindCriteriaGraphWhereDynamic() {

156. // sans critere

157. ContratCriteria contratCriteria = ContratCriteria.builder().build();

158. List<Contrat> contratList = contratRepository.findCriteriaGraphWhereDynamic(contratCriteria);

159. assertTrue(isNotEmpty(contratList));

160. assertGraph(contratList);

161. // critere partiel

162. contratCriteria = ContratCriteria.builder()

163. .contratVersionActif(CONTRAT_VERSION_ACTIF)

164. .suffixAvocatMail(SUFFIX_MAIL)

165. .build();

166. contratList = contratRepository.findCriteriaGraphWhereDynamic(contratCriteria);

167. assertTrue(isNotEmpty(contratList));

168. assertGraph(contratList);

169. // tous les criteres

170. contratCriteria = ContratCriteria.builder()

171. .contratIdList(CONTRAT_ID_LIST)

172. .contratVersionActif(CONTRAT_VERSION_ACTIF)

173. .suffixAvocatMail(SUFFIX_MAIL)

174. .build();

175. contratList = contratRepository.findCriteriaGraphWhereDynamic(contratCriteria);

176. assertTrue(isNotEmpty(contratList));

177. assertGraph(contratList);

178. }

180. @Test

181. @DisplayName("test findProjectionCriteria -> Criteria query with graph, clause where and projection")

182. void testFindCriteriaGraphWhereProjection() {

183. List<ContratProjectionResult> contratProjectionResultList =

184. contratRepository.findCriteriaGraphWhereProjection(CONTRAT_ID_LIST,

185. CONTRAT_VERSION_ACTIF, SUFFIX_MAIL);

186. assertTrue(isNotEmpty(contratProjectionResultList));

187. }

189. @Test

190. @DisplayName("test findNativeWhereProjection -> Native query with with graph, clause where and " +

191. "projection")

192. void testFindNativeGraphWhereProjection() {

193. List<ContratProjectionResult> contratProjectionResultList =

194. contratRepository.findNativeGraphWhereProjection(CONTRAT_ID_LIST,

195. CONTRAT_VERSION_ACTIF, SUFFIX_MAIL);

196. assertTrue(isNotEmpty(contratProjectionResultList));

197. }

199. @Test

200. @DisplayName("test findByExemple -> QueryByExampleExecutor query")

201. void testFindByExemple() {

202. ExampleMatcher matcher = ExampleMatcher.matching().withIgnorePaths("id", "societeId");

203. Contrat contratExample = new Contrat();

204. // matcher exact

205. contratExample.setNom("contrat1");

206. matcher = matcher.withMatcher(Contrat_.NOM, GenericPropertyMatcher::exact);

207. List<Contrat> contratList = contratRepository.findAll(Example.of(contratExample, matcher));

208. assertTrue(isNotEmpty(contratList));

209. assertEquals(1, contratList.size());

210. // matcher startWith

211. contratExample.setNom("contrat");

212. matcher = matcher.withMatcher(Contrat_.NOM, GenericPropertyMatcher::startsWith);

213. contratList = contratRepository.findAll(Example.of(contratExample, matcher));

214. assertTrue(isNotEmpty(contratList));

215. assertEquals(3, contratList.size());

216. }

218. private void assertNoGraph(Contrat contrat) {

219. assertSocieteNotLoaded(contrat.getSociete());

220. assertContratVersionNotLoaded(contrat.getContratVersionSet());

221. assertContratStatutNotLoaded(contrat.getContratStatutSet());

222. }

224. private void assertNoGraph(List<Contrat> contratList) {

225. assertTrue(isNotEmpty(contratList));

226. contratList.forEach(this::assertNoGraph);

227. }

229. private void assertGraph(List<Contrat> contratList) {

230. contratList.forEach(contrat -> {

231. assertSocieteLoaded(contrat.getSociete());

232. assertPersonneLoaded(contrat.getSociete().getAvocat());

233. assertAdresseMailLoaded(contrat.getSociete().getAvocat().getAdresseMailSet());

234. assertPersonneLoaded(contrat.getSociete().getPresident());

235. assertAdresseMailNotLoaded(contrat.getSociete().getPresident().getAdresseMailSet());

236. assertContratVersionLoaded(contrat.getContratVersionSet());

237. assertContratStatutNotLoaded(contrat.getContratStatutSet());

238. }

239. );

240. }

242. private void assertSocieteNotLoaded(Societe societe) {

243. assertThrows(LazyInitializationException.class, () -> societe.setId(1L), "graph societe is loaded");

244. }

246. private void assertSocieteLoaded(Societe societe) {

247. try {

248. societe.setId(1L);

249. } catch (LazyInitializationException e) {

250. fail("graph societe not loaded");

251. }

252. }

254. private void assertPersonneLoaded(Personne personne) {

255. try {

256. personne.setId(1L);

257. } catch (LazyInitializationException e) {

258. fail("graph personne not loaded");

259. }

260. }

262. private void assertContratVersionNotLoaded(Set<ContratVersion> contratVersionSet) {

263. assertThrows(LazyInitializationException.class, contratVersionSet::iterator, "graph contratVersion " +

264. "is loaded");

265. }

267. private void assertContratVersionLoaded(Set<ContratVersion> contratVersionSet) {

268. try {

269. contratVersionSet.forEach(contratVersion -> contratVersion.setId(1L));

270. } catch (LazyInitializationException e) {

271. fail("graph contratVersion not loaded");

272. }

273. }

275. private void assertContratStatutNotLoaded(Set<ContratStatut> contratStatuts) {

276. assertThrows(LazyInitializationException.class, contratStatuts::iterator, "graph contratStatut is " +

277. "loaded");

278. }

280. private void assertAdresseMailNotLoaded(Set<AdresseMail> adresseMailSet) {

281. assertThrows(LazyInitializationException.class, adresseMailSet::iterator, "graph adresseMail is " +

282. "loaded");

283. }

285. private void assertAdresseMailLoaded(Set<AdresseMail> adresseMailSet) {

286. try {

287. adresseMailSet.forEach(adresseMail -> adresseMail.setId(1L));

288. } catch (LazyInitializationException e) {

289. fail("graph adresseMail not loaded");

290. }

291. }

293. private void callRepositoryAndAssert(ContratCriteria contratCriteria) {

294. Specification<Contrat> contratSpecification = null;

295. if (isNotEmpty(contratCriteria.getContratIdList())) {

296. contratSpecification = initOrAndSpecification(null,

297. ContratSpecification.idIn(contratCriteria.getContratIdList()));

298. }

299. if (contratCriteria.getContratVersionActif() != null) {

300. contratSpecification = initOrAndSpecification(contratSpecification,

301. ContratSpecification.contratVersionActifEqual(

302. contratCriteria.getContratVersionActif()));

303. }

304. if (isNotBlank(contratCriteria.getSuffixAvocatMail())) {

305. contratSpecification = initOrAndSpecification(contratSpecification,

306. ContratSpecification.adresseMailLibelleEndWithIgnoreCase(Personne_.AVOCAT,

307. contratCriteria.getSuffixAvocatMail()));

308. }

309. List<Contrat> contratList = contratRepository.findAllWithGraphAndSpecification(GRAPH,

310. contratSpecification);

311. assertTrue(isNotEmpty(contratList));

312. assertGraph(contratList);

313. }

314. }

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 github.

VII - Remerciements

Je tiens à remercier Mickaël Baron, pour sa relecture technique.
Je tiens également à remercier Claude LELOUP, pour sa relecture orthographique.