Note de traduction : Cet article est une traduction française de l’article original de Vlad Mihalcea : The JPA and Hibernate first-level cache
Introduction
Dans cet article, je vais expliquer comment fonctionne le mécanisme de cache de premier niveau JPA et Hibernate et comment il peut améliorer les performances de votre couche d’accès aux données.
En terminologie JPA, le cache de premier niveau est appelé Contexte de Persistance (Persistence Context), et il est représenté par l’interface EntityManager
. Dans Hibernate, le cache de premier niveau est représenté par l’interface Session
, qui étend l’interface JPA EntityManager
.
Les états des entités JPA et les méthodes de transition d’état associées
Une entité JPA peut être dans l’un des états suivants :
- Nouveau (Transient) – État initial, l’entité n’est pas encore gérée
- Géré (Associated) – L’entité est attachée au contexte de persistance
- Détaché (Dissociated) – L’entité n’est plus attachée au contexte
- Supprimé (Deleted) – L’entité est marquée pour suppression
Pour changer l’état de l’entité, vous pouvez utiliser les méthodes persist
, merge
ou remove
de l’EntityManager
JPA.
Lorsque vous appelez la méthode persist
, l’état de l’entité passe de Nouveau à Géré. De même, lors de l’appel de la méthode find
, l’état de l’entité devient également Géré.
Après avoir fermé l’EntityManager
ou appelé la méthode evict
, l’état de l’entité devient Détaché. Quand l’entité est passée à la méthode remove
de l’EntityManager
JPA, l’état de l’entité devient Supprimé.
L’implémentation du cache de premier niveau Hibernate
En interne, Hibernate stocke les entités dans la map suivante :
Map<EntityUniqueKey, Object> entitiesByUniqueKey = new HashMap<>(INIT_COLL_SIZE);
Et l’EntityUniqueKey
est définie comme ceci :
public class EntityUniqueKey implements Serializable { private final String entityName; private final String uniqueKeyName; private final Object key; private final Type keyType; @Override public boolean equals(Object other) { EntityUniqueKey that = (EntityUniqueKey) other; return that != null && that.entityName.equals(entityName) && that.uniqueKeyName.equals(uniqueKeyName) && keyType.isEqual(that.key, key); } }
Quand un état d’entité devient Géré, cela signifie qu’il est stocké dans cette Map
Java entitiesByUniqueKey
.
Ainsi, dans JPA et Hibernate, le cache de premier niveau est une Map
Java, dans laquelle la clé de la Map
est représentée par un objet qui encapsule le nom de l’entité et son identifiant, et la valeur de la Map
est l’objet entité lui-même.
Par conséquent, dans un EntityManager
JPA ou une Session
Hibernate, il ne peut y avoir qu’une seule et unique entité stockée en utilisant le même identifiant et le même type de classe d’entité.
La raison pour laquelle nous pouvons avoir au maximum une représentation d’une entité stockée dans le cache de premier niveau est que, autrement, nous pourrions nous retrouver avec différentes représentations de la même ligne de base de données sans savoir laquelle est la bonne version qui devrait être synchronisée avec l’enregistrement de base de données associé.
Cache transactionnel avec écriture différée
Pour comprendre les avantages de l’utilisation du cache de premier niveau, il est important de comprendre comment fonctionne la stratégie de cache transactionnel avec écriture différée.
Comme déjà expliqué, les méthodes persist
, merge
et remove
de l’EntityManager
JPA changent l’état d’une entité donnée. Cependant, l’état de l’entité n’est pas synchronisé à chaque fois qu’une méthode EntityManager
est appelée. En réalité, les changements d’état ne sont synchronisés que lorsque la méthode flush
de l’EntityManager
est exécutée.
Cette stratégie de synchronisation de cache est appelée écriture différée (write-behind).
L’avantage d’utiliser une stratégie d’écriture différée est que nous pouvons traiter plusieurs entités par lots lors du vidage du cache de premier niveau.
La stratégie d’écriture différée est en fait très commune. Le CPU a également des caches de premier, deuxième et troisième niveau. Et, quand un registre est modifié, son état n’est pas synchronisé avec la mémoire principale à moins qu’un vidage ne soit exécuté.
De plus, un système de base de données relationnelle mappe les pages du système d’exploitation aux pages en mémoire du Buffer Pool, et, pour des raisons de performance, le Buffer Pool est synchronisé périodiquement lors d’un point de contrôle et non à chaque validation de transaction.
Lectures répétables au niveau applicatif
Lorsque vous récupérez une entité JPA, soit directement :
Post post = entityManager.find(Post.class, 1L);
Ou via une requête :
Post post = entityManager.createQuery(""" select p from Post p where p.id = :id """, Post.class) .setParameter("id", 1L) .getSingleResult();
Un LoadEntityEvent
Hibernate va être déclenché. Le LoadEntityEvent
est géré par le DefaultLoadEventListener
, qui va charger l’entité comme suit :
- D’abord, Hibernate vérifie si l’entité est déjà stockée dans le cache de premier niveau, et si c’est le cas, la référence d’entité gérée actuellement est retournée.
- Si l’entité JPA n’est pas trouvée dans le cache de premier niveau, Hibernate vérifiera le cache de second niveau si ce cache est activé.
- Si l’entité n’est pas trouvée dans le cache de premier ou de second niveau, alors Hibernate la chargera depuis la base de données en utilisant une requête SQL.
Le cache de premier niveau fournit une garantie de lectures répétables au niveau applicatif pour les entités car peu importe combien de fois l’entité est chargée depuis le Contexte de Persistance, la même référence d’entité gérée sera retournée à l’appelant.
Quand l’entité est chargée depuis la base de données, Hibernate prend le ResultSet
JDBC et le transforme en un Object[]
Java qui est connu comme l’état chargé de l’entité. L’état chargé est stocké dans le cache de premier niveau avec l’entité gérée.
Comme vous pouvez le voir, le cache de second niveau stocke l’état chargé, donc lors du chargement d’une entité qui était précédemment stockée dans le cache de second niveau, nous pouvons obtenir l’état chargé sans avoir à exécuter la requête SQL associée.
Pour cette raison, l’impact mémoire du chargement d’une entité est plus important que l’objet entité Java lui-même puisque l’état chargé doit également être stocké. Lors du vidage du Contexte de Persistance JPA, l’état chargé sera utilisé par le mécanisme de détection des modifications (dirty checking) pour déterminer si l’entité a changé depuis qu’elle a été chargée pour la première fois. Si l’entité a changé, un UPDATE SQL sera généré.
Donc, si vous ne prévoyez pas de modifier l’entité, alors il est plus efficace de la charger en mode lecture seule car l’état chargé sera supprimé après l’instanciation de l’objet entité.
Conclusion
Le cache de premier niveau est une construction obligatoire dans JPA et Hibernate. Puisque le cache de premier niveau est lié au thread en cours d’exécution, il ne peut pas être partagé entre plusieurs utilisateurs. Pour cette raison, le cache de premier niveau JPA et Hibernate n’est pas thread-safe.
En plus de fournir des lectures répétables au niveau applicatif, le cache de premier niveau peut traiter plusieurs instructions SQL par lots au moment du vidage, améliorant ainsi le temps de réponse des transactions de lecture-écriture.
Cependant, bien qu’il empêche plusieurs appels find
de récupérer la même entité depuis la base de données, il ne peut pas empêcher une requête JPQL ou SQL de charger le dernier instantané de l’entité depuis la base de données, seulement pour l’écarter lors de l’assemblage du jeu de résultats de la requête.
Ping : Mise à jour et suppression en masse avec JPA et Hibernate - java-facile.fr
Ping : Quarkus SFTP : Système de Fichiers Sécurisé avec Dev Services - java-facile.fr