La norme JPA prévoit de représenter les liens entre les tables via les annotations @OneToOne
, @ManyToOne
, @OneToMany
et @ManyToMany
. Par défaut, il faut définir si ces liens seront chargés à la demande ou obligatoirement. Mais peut-on faire autrement?
Présentation du modèle
Dans cet article, je vais prendre un modèle simple à deux tables: un utilisateur possède des bookmarks.
Entity User
1import javax.persistence.*;
2import java.io.Serializable;
3import java.util.Date;
4import java.util.Objects;
5
6@Entity
7@Table(name = "PUBLIC.PUBLIC.USERS")
8@IdClass(UserPK.class)
9public class User implements Serializable {
10 @Id @Column(name="USERID") private String userid;
11
12 public User() { }
13
14 public String getUserid() { return userid; }
15 public void setUserid(String userid) { this.userid = userid; }
16}
Ici rien de spécial, on définit un utilisateur avec un identifiant.
Entity Bookmark
1import javax.persistence.*;
2import java.io.Serializable;
3import java.util.Date;
4import java.util.Objects;
5
6// Decrit un bookmark
7@Entity
8@Table(name = "PUBLIC.PUBLIC.BOOKMARKS")
9@IdClass(BookmarkPK.class)
10public class Bookmark implements Serializable {
11 @Id @Column(name="BREF") private String ref; // Identifiant du bookmark
12 @Column(name="TITLE") private String title; // titre du bookmark
13 @Column(name="URI") private String uri; // URI du bookmark
14 @Column(name="USERID") private String user; // identifiant de l'utilisateur proprietaire de ce bookmark
15
16
17 public Bookmark() { }
18
19 public String getRef() { return ref; }
20 public void setRef(String ref) { this.ref = ref; }
21
22 public String getTitle() { return title; }
23 public void setTitle(String title) { this.title = title; }
24
25 public String getUri() { return uri; }
26 public void setUri(String uri) { this.uri = uri; }
27
28 public String getUser() { return user; }
29 public void setUser(String user) { this.user = user; }
30}
Dans cette entité, on note que le bookmark est la propriété d’un utilisateur.
Repository BookmarkRepository
Pour mémoire, dans Apache Deltaspike, un repository est un service d’accès aux données.
Dans le repository BookmarkRepository, on a ajouté la méthode:
Cette méthode liste les bookmarks pour l’utilisateur indiqué (“nothing fancy here”).
Sans mapping
Lorsqu’on cherche la liste des bookmarks d’un utilisateur via la méthode findByUser, la requête générée est la suivante:
1SELECT t0.BREF, t0.TITLE, t0.URI, t0.USERID FROM PUBLIC.BOOKMARKS t0 WHERE (t0.USERID = ?)
Comme aucun mapping n’est indiqué, la requête cherche uniquement dans la table des bookmarks, comme on pouvait s’en douter. Sauf qu’on ne pourra pas présenter d’informations sur l’utilisateur puisqu’on récupéré uniquement des bookmarks. On va alors ajouter un mapping (c’est à dire une entité liée) vers l’utilisateur.
Avec mapping
Deux possibilités s’offrent à nous: mapping avec chargement explicite (à la demande donc) ou mapping avec chargement forcé.
Mapping explicite “Lazy”
Dans la classe Bookmark, on ajoute le mapping vers le User grâce aux lignes suivantes:
La même recherche produit la même requête!
1SELECT t0.BREF, t0.ADDED, t0.DELETED, t0.USERID, t0.TITLE, t0.UPDATED, t0.URI FROM PUBLIC.BOOKMARKS t0 WHERE (t0.USERID = ?)
C’est logique puisqu’on a configuré un chargement explicite du mapping (FetchType.LAZY
).
On va donc modifier l’entity pour passer en chargement obligatoire.
Mapping forcé “Eager”
Ici on a remplacé “ @ManyToOne(fetch = FetchType.LAZY)
” par “ @ManyToOne(fetch = FetchType.EAGER)
”, provoquant le chargement forcé de l’entity liée.
La requête générée devient alors:
1SELECT t0.BREF, t1.USERID, t0.TITLE, t0.URI, t0.USERID FROM PUBLIC.BOOKMARKS t0 LEFT OUTER JOIN PUBLIC.USERS t1 ON t0.USERID = t1.USERID WHERE (t0.USERID = ?)
La requête a la structure attendue pour un tel mapping.
Problème avec “Lazy” et “Eager”
La notion de chargement explicite ou forcée étant attachée au mapping lui-même, il est impossible de changer le fonctionnement pendant l’exécution. En effet, on n’a pas forcément toujours besoin d’un mapping annoté “Eager” mais il impossible d’éviter son chargement. A contrario, on a parfois besoin d’un mapping annoté “Lazy”: dans ce cas, on peut faire un “touch” pour provoquer le chargement mais JPA génère une requête supplémentaire au lieu de modifier la requête principale. Dans les deux cas (Lazy et Eager), la performance générale subit ces requêtes intempestives.
EntityGraph, le sauveur
La norme JPA 2.1 a ajoutée EntityGraph, qui permet justement de naviguer entre “Lazy” et “Eager”: on peut choisir pour chaque requête quels mappings on veut charger.
Mise en place
Modification de l’entité
Dans la classe Bookmark, on ajoute un EntityGraph nommé:
L’annotation @NamedEntityGraph
comporte deux paramètres:
- name: le nom de l’EntityGraph
- attributeNodes: la liste des mappings à charger
Ensuite on repasse le mapping en FetchType.LAZY
:
En procédant ainsi, on pourra charger l’entité Bookmark avec ou sans mapping User.
Modification du repository
Dans le repository BookmarkRepository, on modifie la méthode findByUser
comme ceci:
L’annotation @EntityGraph
, via son paramètre value, fait le lien avec le profil définit dans l’entity par @NamedEntityGraph
.
La requête générée devient alors celle attendue:
1SELECT t0.BREF, t0.ADDED, t0.DELETED, t1.USERID, t1.EMAIL, t1.TOTP, t1.VALIDTO, t0.TITLE, t0.UPDATED, t0.URI, t0.USERID FROM PUBLIC.BOOKMARKS t0 LEFT OUTER JOIN PUBLIC.USERS t1 ON t0.USERID = t1.USERID WHERE (t0.USERID = ?)
Ensuite, on peut ajouter une méthode findByUserNM
sans annoation @EntityGraph:
La requête générée est alors celle d’origine:
1SELECT t0.BREF, t0.TITLE, t0.URI, t0.USERID FROM PUBLIC.BOOKMARKS t0 WHERE (t0.USERID = ?)
Conclusion
EntityGraph permet de gérer des profils de chargement des mappings et génère des requêtes optimisées. Cela permet d’améliorer assez facilement la performance générale des applications.