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
|
|
Ici rien de spécial, on définit un utilisateur avec un identifiant.
Entity Bookmark
|
|
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:
|
|
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!
|
|
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:
|
|
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:
|
|
Ensuite, on peut ajouter une méthode findByUserNM
sans annoation @EntityGraph:
|
|
La requête générée est alors celle d’origine:
|
|
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.