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:

    
1@Transactional(Transactional.TxType.REQUIRED)
2public abstract List<Bookmark> findByUser(String user);

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:

    
1@ManyToOne(fetch = FetchType.LAZY)
2@JoinColumn(name="USERID", referencedColumnName="USERID")
3private User owner;

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

    
1@IdClass(BookmarkPK.class)
2@NamedEntityGraph(name = "Bookmark-owner", attributeNodes = { @NamedAttributeNode("owner") } )
3public class Bookmark implements Serializable {

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:

    
1@ManyToOne(fetch = FetchType.LAZY)
2@JoinColumn(name="USERID", referencedColumnName="USERID")
3private User owner;

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:

    
1@Transactional(Transactional.TxType.REQUIRED)
2@EntityGraph(value = "Bookmark-owner")
3public abstract List<Bookmark> findByUser(String user);

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:

    
1@Transactional(Transactional.TxType.REQUIRED)
2public abstract List<Bookmark> findByUser(String user);

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.