Tutoriel pour apprendre où définir au mieux une NamedQuery JPA

François-Xavier Robin

Je vais vous présenter succinctement les deux façons natives en JPA pour déclarer des NamedQuery.

On verra alors que ces deux solutions ont des défauts et je vous proposerai alors une troisième qui me paraît plus satisfaisante, fondée sur une « enum ».

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

- Titre : Tutoriel pour apprendre où définir au mieux une NamedQuery JPA

- Auteur : François-Xavier Robin

- Parution : 21 mai 2020

- 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 François-Xavier Robin. 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. Rappels sur les NamedQuery

- II. Où placer nativement une NamedQuery ?

- III. Solution avec une « enum »

- IV. On plante le décor...

- V. Domaine

- VI. Données de test

- VII. Le référenceur programmatique de NamedQuery

- VIII. Au résultat

- IX. Avantages, conclusions et reste à faire...

- X. Remerciements

I - Rappels sur les NamedQuery

Une NamedQuery est tout simplement une requête JPQL associée à un nom qui permet de l'identifier, comme la clef d'une Map.

Pour mémoire, une requête JPQL permet d’interagir avec le modèle persistant d'un point de vue « Objet ». On ne travaille pas directement sur les tables et les colonnes de la base de données, mais sur les classes et les attributs.

La différence principale avec une Query normale concerne la phase d'analyse syntaxique JPQL et sa traduction en un PreparedStatement JDBC, c'est à dire du SQL préparamétré.

En effet une NamedQuery sera analysée en avance de phase et ainsi conservée dans un dictionnaire (sorte de Map) de requêtes.

Pour des raisons de performances, notamment lors de traitements batch JPA, il vaut donc mieux privilégier des NamedQuery.

Enfin, pour des raisons de maintenabilité, il est préférable de sortir les définitions de requêtes JPQL de l'exécution du code.

Quant à la construction dynamique de requêtes, je conseille plutôt d'utiliser Criteria que des concaténations de chaînes de caractères contenant du JPQL.

II - Où placer nativement une NamedQuery ?

JPA offre nativement deux endroits pour déclarer des NamedQuery :

- soit sur une classe annotée avec @Entity, avec l'usage d'une ou plusieurs annotations @NamedQuery. On peut aussi les mettre sur une classe annotée avec @MappedSuperclass mais cette classe doit réellement être héritée pour que les NamedQuery soient prises en compte ;

- soit dans le fichier persistence.xml au moyen du tag <mapping-file>orm.xml</mapping-file> et donc d'un fichier orm.xml.

Mais cela n'est pas vraiment idéal, car :

- dans le premier cas, il faut choisir la classe qui représente une entité et lui faire porter la requête...
Ce n'est pas très logique : une entité est censée représenter une instance et pas travailler sur plusieurs instances. De plus, l'identifiant de la NamedQuery est spécifié sous forme textuelle, ce qui n'est pas particulièrement pratique pour l'autocomplétion et pour vérifier que tout fonctionne avant un lancement RUNTIME. On peut régler éventuellement ce problème avec des constantes String classiques ;

- dans le second cas, on ne pollue pas la classe qui représente l'entité. La déclaration se fait donc « à l'ancienne » dans un fichier XML, ce qui est plutôt une idée acceptable, mais dans ce cas toujours pas d'autocomplétion, sauf à passer par une constante classique qui reprendra exactement le même nom utilisé que dans le fichier XML. Mais on n'est toujours pas à l'abri d'une erreur qu'on ne découvrira encore une fois qu'au RUNTIME.

Je vais donc vous présenter une autre solution...

III - Solution avec une « enum »

L'idée générale est de pouvoir obtenir une NamedQuery au moyen d'une définition dans une enum.

De cette manière, on bénéficiera d'emblée de l'autocomplétion. De plus, on va faire porter à l'enum la génération automatique de l'identificateur de la NamedQuery : comme ça, plus d'erreur possible ! De toute façon, cet identificateur ne servira qu'en interne de la solution.

Enfin, au démarrage de l'application, on référencera les requêtes JPQL avec leur identificateur en tant que NamedQuery dans l'EntityManagerFactory. Ce dernier point sera effectué au moyen de l'EntityManager courant.

IV - On plante le décor...

Afin de comprendre où l’on va, voici ce que l'on cherche à obtenir dans une façade qui appellera les différentes NamedQuery :

1. TypedQuery<VideoGame> query = em.createNamedQuery(VideoGameQuery.FIND_BY_GENRE.getIdentifier(), VideoGame.class);

ou encore :

1. TypedQuery<VideoGame> query = em.createNamedQuery(VideoGameQuery.FIND_BY_NAME_LIKE.getIdentifier(), VideoGame.class);

Ces lignes de code seront utilisées au sein des méthodes dans une façade que le programme principal appellera. La variable em contient une référence vers l'entity manager courant :

1. // les jeux de type SHOOT_THEM_UP

2. System.out.println("Jeux : Shoot them up");

3. FacadeVideoGame.findByGenre(em, GameGenre.SHOOT_THEM_UP).forEach(System.out::println);

4. // les jeux commençant par "Rick"

5. System.out.println("Jeux : commençant par Rick");

6. FacadeVideoGame.findByNameLike(em, "Rick%").forEach(System.out::println);

V - Domaine

Pour joindre l'utile à l'agréable, j'ai pris ici comme entité persistante la représentation d'un jeu vidéo et d'un genre de jeu.

J'utilise aussi Lombok pour simplifier l'écriture des classes.

1. @Entity

2. @Table(name="VIDEO_GAME")

3. @ToString(of = { "id", "name", "gameGenre" })

4. @NoArgsConstructor

5. public class VideoGame implements Serializable

6. {

7. @GeneratedValue

8. @Id

9. @Getter

10. private Long id;

12. @Getter

13. @Setter

14. private String name;

15. @Enumerated(EnumType.STRING)

16. @Getter

17. @Setter

18. @Column(name="GAME_GENRE")

19. private GameGenre gameGenre;

20. public VideoGame(String name, GameGenre gameGenre)

21. {

22. super();

23. this.name = name;

24. this.gameGenre = gameGenre;

25. }

26. }

Et voici l'enum utilisée pour le genre du jeu :

1. public enum GameGenre

2. {

3. RPG, FPS, SHOOT_THEM_UP, ARCADE, PLATFORM, RACING;

4. }

Les interactions seront donc effectuées au moyen de la façade, dont on a aperçu déjà quelques lignes de code précédemment :

1. public final class FacadeVideoGame

2. {

3. private FacadeVideoGame()

4. {

5. // protection du constructeur.

6. }

7. public static List <VideoGame> findByGenre(EntityManager em, GameGenre genre)

8. {

9. // création de la namedQuery, identifiée par sa valeur dans l'enum.

10. TypedQuery<VideoGame> query = em.createNamedQuery(VideoGameQuery.FIND_BY_GENRE.getIdentifier(), VideoGame.class);

11. query.setParameter("gameGenre", genre);

12. return query.getResultList();

13. }

14. public static List <VideoGame> findByNameLike(EntityManager em, String nameLike)

15. {

16. // création de la namedQuery, identifiée par sa valeur dans l'enum.

17. TypedQuery<VideoGame> query = em.createNamedQuery(VideoGameQuery.FIND_BY_NAME_LIKE.getIdentifier(), VideoGame.class);

18. query.setParameter("name", nameLike);

19. return query.getResultList();

20. }

21. }

VI - Données de test

Afin de disposer de données de test, voici l'ensemble des classes utilisées pour peupler la base de données.

J'aurais pu utiliser un script SQL d'initialisation, mais je n'en ai pas eu envie. L'envie est parfois très importante dans la réalisation d'une solution :-).

Le programme principal et complet est le suivant :

1. public class MainProg

2. {

3. public static void main(String[] args)

4. {

5. // 1ère étape : récupération d'un EntityManager et peuplement de données exemples.

6. EntityManager em = ApplicationSingleton.createEntityManager();

7. DataPopulator.populate(em);

9. // Création de l'enregistreur de query JPQL et enregistrement de celle de l'enum.

10. QueryRegistrator.build(em).register(VideoGameQuery.values());

11. // on appelle la façade pour obtenir les jeux de type SHOOT_THEM_UP

12. System.out.println("Jeux : Shoot them up");

13. FacadeVideoGame.findByGenre(em, GameGenre.SHOOT_THEM_UP).forEach(System.out::println);

14. // on appelle la façade pour obtenir les jeux commençant par "Rick"

15. System.out.println("Jeux : commençant par Rick");

16. FacadeVideoGame.findByNameLike(em, "Rick%").forEach(System.out::println);

17. em.close();

18. }

19. }

Voici le « DataPopulator » utilisé dans le programme principal :

1. public class DataPopulator

2. {

3. static void populate(EntityManager em)

4. {

5. // Best ATARI-ST Games ever !

6. List<VideoGame> data = ListPopulator.start()

7. .add("Xenon", GameGenre.SHOOT_THEM_UP)

8. .add("Xenon 2", GameGenre.SHOOT_THEM_UP)

9. .add("Rick Dangerous", GameGenre.PLATFORM)

10. .add("Rick Dangerous 2", GameGenre.PLATFORM)

11. .add("Stunt Car Racer", GameGenre.RACING)

12. .build();

13. // on les rend persistants en base via l'entity manager.

14. em.getTransaction().begin();

15. data.forEach(em::persist);

16. em.getTransaction().commit();

17. }

18. }

Cette classe utilise ListPopulator que voici ci-dessous. Cette classe n'est pas essentielle, mais j'avais envie de m'amuser un peu avec un Builder de List...

1. public class ListPopulator

2. {

3. private List <VideoGame> data = new LinkedList<>();

5. private ListPopulator() {}

6. public static ListPopulator start()

7. {

8. return new ListPopulator();

9. }

10. public ListPopulator add(String name, GameGenre gameGenre)

11. {

12. data.add(new VideoGame(name, gameGenre));

13. return this;

14. }

15. public List<VideoGame> build()

16. {

17. return new ArrayList<>(data);

18. }

19. }

Enfin, dans le cadre de ce test, j'utilise une base de données embarquée H2.

Voici donc mon pom.xml :

1. <properties>

2. <maven.compiler.source>1.8</maven.compiler.source>

3. <maven.compiler.target>1.8</maven.compiler.target>

4. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

5. </properties>

8. <dependencies>

9. <dependency>

10. <groupId>javax.persistence</groupId>

11. <artifactId>javax.persistence-api</artifactId>

12. <version>2.2</version>

13. </dependency>

14. <dependency>

15. <groupId>org.eclipse.persistence</groupId>

16. <artifactId>org.eclipse.persistence.jpa</artifactId>

17. <version>2.7.1</version>

18. <scope>runtime</scope>

19. </dependency>

20. <dependency>

21. <groupId>com.h2database</groupId>

22. <artifactId>h2</artifactId>

23. <version>1.4.196</version>

24. <scope>runtime</scope>

25. </dependency>

26. <dependency>

27. <groupId>org.projectlombok</groupId>

28. <artifactId>lombok</artifactId>

29. <version>1.16.20</version>

30. <scope>provided</scope>

31. </dependency>

32. </dependencies>

et voici la déclaration du persistence-unit du fichier persistence.xml :

1. <persistence-unit name="named-queries-demo" transaction-type="RESOURCE_LOCAL">

2. <exclude-unlisted-classes>false</exclude-unlisted-classes>

3. <properties>

4. <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>

5. <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:test"/>

6. <property name="javax.persistence.jdbc.user" value="sa"/>

7. <property name="javax.persistence.schema-generation.database.action" value="create"/>

8. <property name="eclipselink.logging.level" value="FINE"/>

9. <property name="eclipselink.logging.thread" value="false"/>

10. <property name="eclipselink.logging.timestamp" value="false"/>

11. <property name="eclipselink.logging.exceptions" value="false"/>

12. </properties>

13. </persistence-unit>

14. </persistence>

Oui, j'aurais pu aussi le faire avec des tests unitaires JUnit, mais je n'en avais toujours pas envie :-)

VII - Le référenceur programmatique de NamedQuery

Je suis d'accord, le titre de ce paragraphe est un peu pompeux, mais je n'ai pas trouvé mieux pour le moment. Si vous avez une meilleure idée, faites-m'en part en commentaires.

Ce diagramme UML représente le système mis en place. La classe MainProg n'est là que pour faire référencer au QueryRegistrator l'ensemble des valeurs de l'enum.

la seconde méthode « register(RegistrableQuery... queries) » est bien implémentée au moyen d'un varsargs et non pas d'un tableau, comme représenté dans ce diagramme.

En premier lieu, voici l'interface RegistrableQuery qui sera implémentée par l'enum et qui garantira le comportement attendu par chacune des valeurs :

1. public interface RegistrableQuery

2. {

3. /**

4. * @return la requête JPQL.

5. */

6. String getQuery();

7. /**

8. * @return l'identifiant de la requête JPQL.

9. */

10. String getIdentifier();

11. }

Voici ENFIN l'enum qui porte nos requêtes JPQL qui vont devenir des NamedQuery :

1. public enum VideoGameQuery implements RegistrableQuery

2. {

3. /**

4. * retourne les VideoGame en fonction de leur genre.

5. * Argument JPQL attendu : gameGenre de type GameGenre.

6. */

7. FIND_BY_GENRE("SELECT vg FROM VideoGame vg WHERE vg.gameGenre = :gameGenre"),

9. /**

10. * retourne les VideoGame en fonction d'un nom approchant (LIKE).

11. * Argument JPQL attendu : name de type String.

12. */

13. FIND_BY_NAME_LIKE("SELECT vg FROM VideoGame vg WHERE vg.name LIKE :name");

16. // partie "technique"

18. /**

19. * String JPQL de la requête

20. */

21. final String query;

23. /**

24. * constructeur pour chaque valeur de l'enum.

25. *

26. * @param returnedClass

27. * @param query

28. */

29. private VideoGameQuery(String query)

30. {

31. this.query = query;

32. }

33. /**

34. * retourne la requête JPQL

35. */

36. @Override

37. public String getQuery()

38. {

39. return this.query;

40. }

41. /**

42. * construit et retourne l'identifiant de la requête JPQL qui sert de clef pour

43. * la namedQuery.

44. */

45. @Override

46. public String getIdentifier()

47. {

48. return String.format("%s_%s", this.getClass(), this.name());

49. }

50. }

Ensuite, voici notre référenceur programmatique (registrator) :

1. @Log

2. public final class QueryRegistrator

3. {

4. private EntityManager em;

6. private QueryRegistrator()

7. {

8. // protection du constructeur

9. }

11. public static QueryRegistrator build(EntityManager em)

12. {

13. QueryRegistrator qr = new QueryRegistrator();

14. qr.em = em;

15. return qr;

16. }

18. /**

19. * enregistre la requête auprès de l'EntityManagerFactory.

20. * Cette requête deviendra alors une NamedQuery accessible via son enum.

21. *

22. * @param query

23. * @return instance courante pour permettre du method chaining.

24. */

25. public QueryRegistrator register(RegistrableQuery query)

26. {

27. Query realQuery = this.em.createQuery(query.getQuery());

28. EntityManagerFactory emf = em.getEntityManagerFactory();

29. emf.addNamedQuery(query.getIdentifier(), realQuery);

30. if (log.isLoggable(Level.INFO))

31. {

32. log.info(String.format("Registered : %s >> %s", query.getIdentifier(), realQuery));

33. }

34. return this;

35. }

36. /**

37. * enregistre plusieurs requêtes auprès de l'EntityManagerFactory.

38. * Cette requête deviendra alors une NamedQuery accessible via son enum.

39. *

40. * @param queries

41. * @return instance courante pour permettre du method chaining.

42. */

43. public QueryRegistrator register(RegistrableQuery... queries)

44. {

45. Stream.of(queries).forEach(this::register);

46. return this;

47. }

48. }

C'est bien cette classe et sa méthode register(RegistrableQuery query) qui fait tout le travail. La méthode register(RegistrableQuery... queries) permettra d'inscrire toutes les valeurs de l'enum d'un coup. Souvenez-vous, c'était dans le programme principal :

1. QueryRegistrator.build(em).register(VideoGameQuery.values());

VIII - Au résultat

Voici ce que l'on obtient dans la console avec le niveau de log fixé à FINE :

1. mars 12, 2018 11:16:58 AM demo.registrator.QueryRegistrator register

2. INFOS: Registered : class demo.model.VideoGameQuery_FIND_BY_GENRE >> EJBQueryImpl(ReadAllQuery(referenceClass=VideoGame sql="SELECT ID, GAME_GENRE, NAME FROM VIDEO_GAME WHERE (GAME_GENRE = ?)"))

3. mars 12, 2018 11:16:58 AM demo.registrator.QueryRegistrator register

4. INFOS: Registered : class demo.model.VideoGameQuery_FIND_BY_NAME_LIKE >> EJBQueryImpl(ReadAllQuery(referenceClass=VideoGame sql="SELECT ID, GAME_GENRE, NAME FROM VIDEO_GAME WHERE NAME LIKE ?"))

5. Jeux : Shoot them up

6. bind => [SHOOT_THEM_UP]

7. VideoGame(id=1, name=Xenon, gameGenre=SHOOT_THEM_UP)

8. VideoGame(id=2, name=Xenon 2, gameGenre=SHOOT_THEM_UP)

9. Jeux : commençant par Rick

10. bind => [Rick%]

11. VideoGame(id=3, name=Rick Dangerous, gameGenre=PLATFORM)

12. VideoGame(id=4, name=Rick Dangerous 2, gameGenre=PLATFORM)

Les requêtes JPQL ont bien été parcourues en avance de phase et inscrites auprès de l'EntityManagerFactory. Elles sont bien converties en PreparedStatement, comme prévu.

IX - Avantages, conclusions et reste à faire...

Les requêtes JPQL NamedQuery ne sont maintenant :

- ni perdues au sein d'une méthode ;

- ni mal placées sur la déclaration d'une entité persistante ;

- ni sans liaison directe au sein d'un fichier orm.xml.

On a gagné :

- en découplage ;

- en autocomplétion ;

- en réutilisation ;

- en maintenance (les NamedQuery sont centralisées).

Il restera, pour améliorer le système, à prendre en compte les QueryHints et LockMode : cela pourra être codé au niveau de l'enum.

On pourra aussi faire porter à l'enum la classe métier « de travail » et créer une classe utilitaire pour créer automatiquement des « RegistrableQuery » sans avoir à le faire nous même. J'ai préféré cette approche pour ne pas bouleverser toutes les pratiques d'instanciation de NamedQuery déjà éventuellement en place.

Enfin, le référencement pourra se faire de manière automatique au démarrage, au moyen d'un singleton dédié. Par exemple :

- un EJB Singleton annoté avec @Singleton et @Startup ;

- ou un Bean CDI annoté avec @ApplicationScoped avec un observateur sur ApplicationScoped.class : https://rmannibucau.wordpress.com/2015/03/10/cdi-and-startup/.

Enfin, cela me permettra d'embrayer sur un nouveau post relatif à « Spring Data JPA » versus « CDI DeltaSpike Data Module », pour voir que finalement on peut presque se passer de la définition de @NamedQuery à l'ancienne avec ces deux bibliothèques ! Nom de Zeus !

Comme on dit à Hill Valley...

N'hésitez pas à formuler des remarques ou poser des questions dans les commentaires afin d'améliorer la clarté de ce qui est présenté, voire d'améliorer et/ou de simplifier l'ensemble.

Vous pouvez retrouver l'intégralité du code source de ce projet sur mon compte GitHub.

X - Remerciements

Cet article a été publié avec l'aimable autorisation François-Xavier Robin.

Nous tenons à remercier escartefigue pour sa relecture orthographique attentive de cet article et Mickael Baron pour la mise au gabarit.