Cache de deuxième niveau Hibernate

1. Vue d'ensemble

L'un des avantages des couches d'abstraction de base de données telles que les frameworks ORM (object-relational mapping) est leur capacité à mettre en cache de manière transparente les données extraites du magasin sous-jacent. Cela permet d'éliminer les coûts d'accès à la base de données pour les données fréquemment consultées.

Les gains de performances peuvent être importants si les taux de lecture / écriture du contenu mis en cache sont élevés, en particulier pour les entités constituées de grands graphiques d'objets.

Dans cet article, nous explorons le cache de deuxième niveau Hibernate.

Nous expliquons quelques concepts de base et comme toujours nous illustrons tout avec des exemples simples. Nous utilisons JPA et revenons à l'API native Hibernate uniquement pour les fonctionnalités qui ne sont pas standardisées dans JPA.

2. Qu'est-ce qu'un cache de deuxième niveau?

Comme la plupart des autres frameworks ORM entièrement équipés, Hibernate a le concept de cache de premier niveau. Il s'agit d'un cache à portée de session qui garantit que chaque instance d'entité n'est chargée qu'une seule fois dans le contexte persistant.

Une fois la session fermée, le cache de premier niveau est également terminé. C'est en fait souhaitable, car cela permet aux sessions simultanées de fonctionner avec des instances d'entité isolées les unes des autres.

D'autre part, le cache de second niveau est de type SessionFactory , ce qui signifie qu'il est partagé par toutes les sessions créées avec la même fabrique de sessions. Lorsqu'une instance d'entité est recherchée par son identifiant (soit par la logique de l'application, soit par Hibernate en interne, par exemple lorsqu'elle charge des associations vers cette entité à partir d'autres entités), et si la mise en cache de second niveau est activée pour cette entité, ce qui suit se produit:

  • Si une instance est déjà présente dans le cache de premier niveau, elle en est renvoyée
  • Si une instance n'est pas trouvée dans le cache de premier niveau et que l'état de l'instance correspondante est mis en cache dans le cache de second niveau, les données sont récupérées à partir de là et une instance est assemblée et renvoyée
  • Sinon, les données nécessaires sont chargées à partir de la base de données et une instance est assemblée et renvoyée

Une fois l'instance stockée dans le contexte de persistance (cache de premier niveau), elle en est renvoyée dans tous les appels ultérieurs au sein de la même session jusqu'à ce que la session soit fermée ou que l'instance soit expulsée manuellement du contexte de persistance. En outre, l'état de l'instance chargée est stocké dans le cache L2 s'il n'y était pas déjà.

3. Usine de région

La mise en cache de deuxième niveau Hibernate est conçue pour ne pas connaître le fournisseur de cache réellement utilisé. Hibernate doit uniquement être fourni avec une implémentation de l' interface org.hibernate.cache.spi.RegionFactory qui encapsule tous les détails spécifiques aux fournisseurs de cache réels. Fondamentalement, il agit comme un pont entre Hibernate et les fournisseurs de cache.

Dans cet article, nous utilisons Ehcache comme fournisseur de cache , qui est un cache mature et largement utilisé. Vous pouvez bien sûr choisir n'importe quel autre fournisseur, tant qu'il existe une implémentation d'une RegionFactory pour lui.

Nous ajoutons l'implémentation d'usine de la région Ehcache au chemin de classe avec la dépendance Maven suivante:

 org.hibernate hibernate-ehcache 5.2.2.Final 

Jetez un œil ici pour la dernière version de hibernate-ehcache . Cependant, assurez-vous que la version hibernate-ehcache est égale à la version Hibernate que vous utilisez dans votre projet, par exemple si vous utilisez hibernate-ehcache 5.2.2.Final comme dans cet exemple, alors la version d'Hibernate devrait également être 5.2.2. Finale .

L' artefact hibernate-ehcache a une dépendance sur l'implémentation Ehcache elle-même, qui est donc également incluse de manière transitoire dans le chemin de classe .

4. Activation de la mise en cache de deuxième niveau

Avec les deux propriétés suivantes, nous indiquons à Hibernate que la mise en cache L2 est activée et nous lui donnons le nom de la classe de fabrique de région:

hibernate.cache.use_second_level_cache=true hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory 

Par exemple, dans persistence.xml, cela ressemblerait à:

 ...   ... 

Pour désactiver la mise en cache de deuxième niveau (à des fins de débogage par exemple), définissez simplement la propriété hibernate.cache.use_second_level_cache sur false.

5. Rendre une entité cachable

Afin de rendre une entité éligible pour la mise en cache de deuxième niveau , nous l'annotons avec l' annotation @ org.hibernate.annotations.Cache spécifique à Hibernate et spécifions une stratégie de concurrence de cache.

Certains développeurs considèrent que c'est une bonne convention d'ajouter également l' annotation standard @ javax.persistence.Cacheable (bien que non requise par Hibernate), donc une implémentation de classe d'entité pourrait ressembler à ceci:

@Entity @Cacheable @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public class Foo { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "ID") private long id; @Column(name = "NAME") private String name; // getters and setters }

Pour chaque classe d'entité, Hibernate utilisera une région de cache distincte pour stocker l'état des instances de cette classe. Le nom de la région est le nom complet de la classe.

Par exemple, les instances Foo sont stockées dans un cache nommé com.baeldung.hibernate.cache.model.Foo dans Ehcache.

Pour vérifier que la mise en cache fonctionne, nous pouvons écrire un test rapide comme celui-ci:

Foo foo = new Foo(); fooService.create(foo); fooService.findOne(foo.getId()); int size = CacheManager.ALL_CACHE_MANAGERS.get(0) .getCache("com.baeldung.hibernate.cache.model.Foo").getSize(); assertThat(size, greaterThan(0));

Ici, nous utilisons l'API Ehcache directement pour vérifier que le cache com.baeldung.hibernate.cache.model.Foo n'est pas vide après le chargement d'une instance Foo .

Vous pouvez également activer la journalisation du SQL généré par Hibernate et appeler fooService.findOne (foo.getId ()) plusieurs fois dans le test pour vérifier que l' instruction select pour le chargement de Foo est imprimée une seule fois (la première fois), ce qui signifie que dans les appelle l'instance d'entité est extraite du cache.

6. Stratégie de concurrence du cache

Based on use cases, we are free to pick one of the following cache concurrency strategies:

  • READ_ONLY: Used only for entities that never change (exception is thrown if an attempt to update such an entity is made). It is very simple and performant. Very suitable for some static reference data that don't change
  • NONSTRICT_READ_WRITE: Cache is updated after a transaction that changed the affected data has been committed. Thus, strong consistency is not guaranteed and there is a small time window in which stale data may be obtained from cache. This kind of strategy is suitable for use cases that can tolerate eventual consistency
  • READ_WRITE: This strategy guarantees strong consistency which it achieves by using ‘soft' locks: When a cached entity is updated, a soft lock is stored in the cache for that entity as well, which is released after the transaction is committed. All concurrent transactions that access soft-locked entries will fetch the corresponding data directly from database
  • TRANSACTIONAL: Cache changes are done in distributed XA transactions. A change in a cached entity is either committed or rolled back in both database and cache in the same XA transaction

7. Cache Management

If expiration and eviction policies are not defined, the cache could grow indefinitely and eventually consume all of available memory. In most cases, Hibernate leaves cache management duties like these to cache providers, as they are indeed specific to each cache implementation.

For example, we could define the following Ehcache configuration to limit the maximum number of cached Foo instances to 1000:

8. Collection Cache

Collections are not cached by default, and we need to explicitly mark them as cacheable. For example:

@Entity @Cacheable @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public class Foo { ... @Cacheable @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) @OneToMany private Collection bars; // getters and setters }

9. Internal Representation of Cached State

Entities are not stored in second-level cache as Java instances, but rather in their disassembled (hydrated) state:

  • Id (primary key) is not stored (it is stored as part of the cache key)
  • Transient properties are not stored
  • Collections are not stored (see below for more details)
  • Non-association property values are stored in their original form
  • Only id (foreign key) is stored for ToOne associations

This depicts general Hibernate second-level cache design in which cache model reflects the underlying relational model, which is space-efficient and makes it easy to keep the two synchronized.

9.1. Internal Representation of Cached Collections

We already mentioned that we have to explicitly indicate that a collection (OneToMany or ManyToMany association) is cacheable, otherwise it is not cached.

Actually, Hibernate stores collections in separate cache regions, one for each collection. The region name is a fully qualified class name plus the name of collection property, for example: com.baeldung.hibernate.cache.model.Foo.bars. This gives us the flexibility to define separate cache parameters for collections, e.g. eviction/expiration policy.

Also, it is important to mention that only ids of entities contained in a collection are cached for each collection entry, which means that in most cases it is a good idea to make the contained entities cacheable as well.

10. Cache Invalidation for HQL DML-Style Queries and Native Queries

When it comes to DML-style HQL (insert, update and delete HQL statements), Hibernate is able to determine which entities are affected by such operations:

entityManager.createQuery("update Foo set … where …").executeUpdate();

In this case all Foo instances are evicted from L2 cache, while other cached content remains unchanged.

However, when it comes to native SQL DML statements, Hibernate cannot guess what is being updated, so it invalidates the entire second level cache:

session.createNativeQuery("update FOO set … where …").executeUpdate();

This is probably not what you want! The solution is to tell Hibernate which entities are affected by native DML statements, so that it can evict only entries related to Foo entities:

Query nativeQuery = entityManager.createNativeQuery("update FOO set ... where ..."); nativeQuery.unwrap(org.hibernate.SQLQuery.class).addSynchronizedEntityClass(Foo.class); nativeQuery.executeUpdate();

We have too fall back to Hibernate native SQLQuery API, as this feature is not (yet) defined in JPA.

Note that the above applies only to DML statements (insert, update, delete and native function/procedure calls). Native select queries do not invalidate cache.

11. Query Cache

Results of HQL queries can also be cached. This is useful if you frequently execute a query on entities that rarely change.

To enable query cache, set the value of hibernate.cache.use_query_cache property to true:

hibernate.cache.use_query_cache=true

Then, for each query you have to explicitly indicate that the query is cacheable (via an org.hibernate.cacheable query hint):

entityManager.createQuery("select f from Foo f") .setHint("org.hibernate.cacheable", true) .getResultList();

11.1. Query Cache Best Practices

Here are a some guidelines and best practices related to query caching:

  • As is case with collections, only ids of entities returned as a result of a cacheable query are cached, so it is strongly recommended that second-level cache is enabled for such entities.
  • There is one cache entry per each combination of query parameter values (bind variables) for each query, so queries for which you expect lots of different combinations of parameter values are not good candidates for caching.
  • Queries that involve entity classes for which there are frequent changes in the database are not good candidates for caching either, because they will be invalidated whenever there is a change related to any of the entity classed participating in the query, regardless whether the changed instances are cached as part of the query result or not.
  • By default, all query cache results are stored in org.hibernate.cache.internal.StandardQueryCache region. As with entity/collection caching, you can customize cache parameters for this region to define eviction and expiration policies according to your needs. For each query you can also specify a custom region name in order to provide different settings for different queries.
  • For all tables that are queried as part of cacheable queries, Hibernate keeps last update timestamps in a separate region named org.hibernate.cache.spi.UpdateTimestampsCache. Being aware of this region is very important if you use query caching, because Hibernate uses it to verify that cached query results are not stale. The entries in this cache must not be evicted/expired as long as there are cached query results for the corresponding tables in query results regions. It is best to turn off automatic eviction and expiration for this cache region, as it does not consume lots of memory anyway.

12. Conclusion

Dans cet article, nous avons examiné comment configurer le cache de deuxième niveau Hibernate. Nous avons vu qu'il est assez facile à configurer et à utiliser, car Hibernate fait tout le gros du travail dans les coulisses, rendant l'utilisation du cache de deuxième niveau transparente à la logique métier de l'application.

L'implémentation de ce tutoriel sur le cache de deuxième niveau Hibernate est disponible sur Github. Il s'agit d'un projet basé sur Maven, il devrait donc être facile à importer et à exécuter tel quel.