Caché de segundo nivel de hibernación

1. Información general

Una de las ventajas de las capas de abstracción de bases de datos, como los marcos ORM (mapeo relacional de objetos), es su capacidad para almacenar en caché de forma transparente los datos recuperados del almacén subyacente. Esto ayuda a eliminar los costos de acceso a la base de datos para los datos de acceso frecuente.

Las ganancias de rendimiento pueden ser significativas si las proporciones de lectura / escritura del contenido almacenado en caché son altas, especialmente para entidades que consisten en gráficos de objetos grandes.

En este artículo exploramos la caché de segundo nivel de Hibernate.

Explicamos algunos conceptos básicos y como siempre lo ilustramos todo con ejemplos sencillos. Usamos JPA y recurrimos a la API nativa de Hibernate solo para aquellas características que no están estandarizadas en JPA.

2. ¿Qué es una caché de segundo nivel?

Como la mayoría de los otros marcos ORM completamente equipados, Hibernate tiene el concepto de caché de primer nivel. Es una caché con ámbito de sesión que garantiza que cada instancia de entidad se cargue solo una vez en el contexto persistente.

Una vez que se cierra la sesión, la caché de primer nivel también se termina. Esto es realmente deseable, ya que permite que las sesiones simultáneas funcionen con instancias de entidad de forma aislada.

Por otro lado, la caché de segundo nivel tiene un alcance de SessionFactory , lo que significa que es compartida por todas las sesiones creadas con la misma fábrica de sesiones. Cuando se busca una instancia de entidad por su id (ya sea por la lógica de la aplicación o por Hibernate internamente, por ejemplo , cuando carga asociaciones a esa entidad desde otras entidades), y si el almacenamiento en caché de segundo nivel está habilitado para esa entidad, sucede lo siguiente:

  • Si una instancia ya está presente en la caché de primer nivel, se devuelve desde allí.
  • Si no se encuentra una instancia en la caché de primer nivel, y el estado de la instancia correspondiente se almacena en caché en la caché de segundo nivel, los datos se obtienen de allí y se ensambla y devuelve una instancia.
  • De lo contrario, los datos necesarios se cargan desde la base de datos y se ensambla y devuelve una instancia.

Una vez que la instancia se almacena en el contexto de persistencia (caché de primer nivel), se devuelve desde allí en todas las llamadas posteriores dentro de la misma sesión hasta que se cierra la sesión o se expulsa manualmente la instancia del contexto de persistencia. Además, el estado de la instancia cargada se almacena en la caché L2 si aún no estaba allí.

3. Fábrica de región

El almacenamiento en caché de segundo nivel de Hibernate está diseñado para desconocer el proveedor de caché real utilizado. Hibernate solo necesita contar con una implementación de la interfaz org.hibernate.cache.spi.RegionFactory que encapsula todos los detalles específicos de los proveedores de caché reales. Básicamente, actúa como un puente entre Hibernate y los proveedores de caché.

En este artículo usamos Ehcache como proveedor de caché , que es un caché maduro y ampliamente utilizado. Por supuesto, puede elegir cualquier otro proveedor, siempre que haya una implementación de RegionFactory para él.

Agregamos la implementación de la fábrica de la región Ehcache a la ruta de clase con la siguiente dependencia de Maven:

 org.hibernate hibernate-ehcache 5.2.2.Final 

Eche un vistazo aquí para ver la última versión de hibernate-ehcache . Sin embargo, asegúrese de que la versión de hibernate-ehcache sea ​​igual a la versión de Hibernate que usa en su proyecto, por ejemplo , si usa hibernate-ehcache 5.2.2.Final como en este ejemplo, entonces la versión de Hibernate también debería ser 5.2.2. Final .

El artefacto hibernate-ehcache depende de la propia implementación de Ehcache, que por lo tanto también se incluye de manera transitiva en la ruta de clases.

4. Habilitación del almacenamiento en caché de segundo nivel

Con las siguientes dos propiedades, le decimos a Hibernate que el almacenamiento en caché L2 está habilitado y le damos el nombre de la clase de fábrica de la región:

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

Por ejemplo, en persistence.xml se vería así:

 ...   ... 

Para deshabilitar el almacenamiento en caché de segundo nivel (con fines de depuración, por ejemplo), simplemente establezca la propiedad hibernate.cache.use_second_level_cache en false.

5. Convertir una entidad en caché

Para que una entidad sea elegible para el almacenamiento en caché de segundo nivel , la anotamos con la anotación @ org.hibernate.annotations.Cache específica de Hibernate y especificamos una estrategia de concurrencia de caché.

Algunos desarrolladores consideran que es una buena convención agregar la anotación estándar @ javax.persistence.Cacheable también (aunque no es requerida por Hibernate), por lo que la implementación de una clase de entidad podría verse así:

@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 }

Para cada clase de entidad, Hibernate utilizará una región de caché separada para almacenar el estado de las instancias de esa clase. El nombre de la región es el nombre de la clase completamente calificado.

Por ejemplo, las instancias de Foo se almacenan en un caché llamado com.baeldung.hibernate.cache.model.Foo en Ehcache.

Para verificar que el almacenamiento en caché está funcionando, podemos escribir una prueba rápida como esta:

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));

Aquí usamos la API de Ehcache directamente para verificar que la caché com.baeldung.hibernate.cache.model.Foo no esté vacía después de cargar una instancia de Foo .

También puede habilitar el registro de SQL generado por Hibernate e invocar fooService.findOne (foo.getId ()) varias veces en la prueba para verificar que la declaración de selección para cargar Foo se imprima solo una vez (la primera vez), lo que significa que en las siguientes llama a la instancia de la entidad se obtiene de la caché.

6. Estrategia de concurrencia de caché

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

En este artículo, analizamos cómo configurar la caché de segundo nivel de Hibernate. Vimos que es bastante fácil de configurar y usar, ya que Hibernate hace todo el trabajo pesado detrás de escena, haciendo que la utilización de la caché de segundo nivel sea transparente para la lógica comercial de la aplicación.

La implementación de este tutorial de caché de segundo nivel de Hibernate está disponible en Github. Este es un proyecto basado en Maven, por lo que debería ser fácil de importar y ejecutar tal como está.