Hibernate EHCache – Hibernate Segundo Nivel de Caché

Bienvenido al Tutorial de Ejemplo de Caché de Segundo Nivel de Hibernate. Hoy vamos a explorar Hibernate EHCache, que es el proveedor de caché de segundo nivel más popular para Hibernate.

Caché de Segundo Nivel de Hibernate

Uno de los principales beneficios de usar Hibernate en aplicaciones grandes es su soporte para la caché, reduciendo así las consultas a la base de datos y mejorando el rendimiento. En el ejemplo anterior, examinamos la Caché de Primer Nivel de Hibernate y hoy nos centraremos en la Caché de Segundo Nivel de Hibernate utilizando la implementación de Hibernate EHCache. Los proveedores de caché de segundo nivel de Hibernate incluyen EHCache e Infinispan, pero EHCache es más popular y lo utilizaremos en nuestro proyecto de ejemplo. Sin embargo, antes de pasar a nuestro proyecto, debemos conocer las diferentes estrategias para almacenar en caché un objeto.

  1. Solo Lectura: Esta estrategia de almacenamiento en caché debe usarse para objetos persistentes que siempre se leerán pero nunca se actualizarán. Es ideal para leer y almacenar en caché la configuración de la aplicación y otros datos estáticos que nunca se actualizan. Esta es la estrategia más simple con el mejor rendimiento porque no hay sobrecarga para verificar si el objeto se actualizó en la base de datos o no.
  2. Leer Escribir: Es bueno para objetos persistentes que pueden ser actualizados por la aplicación de Hibernate. Sin embargo, si los datos son actualizados ya sea a través del backend u otras aplicaciones, entonces no hay forma de que Hibernate lo sepa y los datos podrían quedar obsoletos. Así que al usar esta estrategia, asegúrate de utilizar la API de Hibernate para actualizar los datos.
  3. Lectura Escritura No Restringida: Si la aplicación solo necesita ocasionalmente actualizar datos y no se requiere un aislamiento estricto de transacciones, una caché de lectura no estricta podría ser apropiada.
  4. Transaccional: La estrategia de caché transaccional proporciona soporte para proveedores de caché completamente transaccionales como JBoss TreeCache. Esta caché solo se puede utilizar en un entorno JTA y debes especificar hibernate.transaction.manager_lookup_class.

Hibernate EHCache

Dado que EHCache soporta todas las estrategias de caché mencionadas anteriormente, es la mejor opción cuando buscas una caché de segundo nivel en Hibernate. No entraré en muchos detalles sobre EHCache, mi enfoque principal será hacer que funcione para la aplicación de Hibernate. Crea un proyecto Maven en Eclipse o tu IDE favorito, la implementación final se verá como en la siguiente imagen. Veamos cada componente de la aplicación uno por uno.

Dependencias de Hibernate EHCache Maven

Para la caché de segundo nivel de Hibernate, necesitaríamos agregar las dependencias ehcache-core y hibernate-ehcache en nuestra aplicación. EHCache utiliza slf4j para el registro, así que también he agregado slf4j-simple con ese propósito. Estoy utilizando las últimas versiones de todas estas APIs, hay una ligera posibilidad de que las APIs de hibernate-ehcache no sean compatibles con la API ehcache-core, en ese caso necesitarás verificar el archivo pom.xml de hibernate-ehcache para encontrar la versión correcta a utilizar. Nuestro pom.xml final se ve así.

<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.journaldev.hibernate</groupId>
	<artifactId>HibernateEHCacheExample</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<description>Hibernate Secondary Level Cache Example using EHCache implementation</description>

	<dependencies>
		<!-- Hibernate Core API -->
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-core</artifactId>
			<version>4.3.5.Final</version>
		</dependency>
		<!-- MySQL Driver -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.0.5</version>
		</dependency>
		<!-- EHCache Core APIs -->
		<dependency>
			<groupId>net.sf.ehcache</groupId>
			<artifactId>ehcache-core</artifactId>
			<version>2.6.9</version>
		</dependency>
		<!-- Hibernate EHCache API -->
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-ehcache</artifactId>
			<version>4.3.5.Final</version>
		</dependency>
		<!-- EHCache uses slf4j for logging -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-simple</artifactId>
			<version>1.7.5</version>
		</dependency>
	</dependencies>
</project>

Configuración de Hibernate EHCache para la Caché de Segundo Nivel de Hibernate

La caché de segundo nivel de Hibernate está desactivada por defecto, así que necesitaríamos habilitarla y agregar algunas configuraciones para hacer que funcione. Nuestro archivo hibernate.cfg.xml se ve así.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration SYSTEM "classpath://org/hibernate/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
	<session-factory>
		<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
		<property name="hibernate.connection.password">pankaj123</property>
		<property name="hibernate.connection.url">jdbc:mysql://localhost/TestDB</property>
		<property name="hibernate.connection.username">pankaj</property>
		<property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>

		<property name="hibernate.current_session_context_class">thread</property>
		<property name="hibernate.show_sql">true</property>
		
		<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
		
		<!-- For singleton factory -->
		<!-- <property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory</property>
		 -->
		 
		 <!-- enable second level cache and query cache -->
		 <property name="hibernate.cache.use_second_level_cache">true</property>
		 <property name="hibernate.cache.use_query_cache">true</property>
 		 <property name="net.sf.ehcache.configurationResourceName">/myehcache.xml</property>

		<mapping class="com.journaldev.hibernate.model.Employee" />
		<mapping class="com.journaldev.hibernate.model.Address" />
	</session-factory>
</hibernate-configuration>

Algunos puntos importantes sobre las configuraciones de la caché de segundo nivel de Hibernate son:

  1. hibernate.cache.region.factory_class se utiliza para definir la clase Factory para el almacenamiento en caché de segundo nivel, estoy utilizando org.hibernate.cache.ehcache.EhCacheRegionFactory para esto. Si deseas que la clase de la fábrica sea un singleton, debes utilizar la clase org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory. Si estás utilizando Hibernate 3, las clases correspondientes serán net.sf.ehcache.hibernate.EhCacheRegionFactory y net.sf.ehcache.hibernate.SingletonEhCacheRegionFactory.
  2. hibernate.cache.use_second_level_cache se utiliza para habilitar la caché de segundo nivel.
  3. hibernate.cache.use_query_cache se utiliza para habilitar la caché de consultas; sin ella, los resultados de las consultas HQL no se almacenarán en caché.
  4. net.sf.ehcache.configurationResourceName se utiliza para definir la ubicación del archivo de configuración de EHCache; es un parámetro opcional y, si no está presente, EHCache intentará localizar el archivo ehcache.xml en el classpath de la aplicación.

Archivo de Configuración de Hibernate EHCache

Nuestro archivo de configuración de EHCache, myehcache.xml, se ve así:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
	xsi:noNamespaceSchemaLocation="ehcache.xsd" updateCheck="true"
	monitoring="autodetect" dynamicConfig="true">

	<diskStore path="java.io.tmpdir/ehcache" />

	<defaultCache maxEntriesLocalHeap="10000" eternal="false"
		timeToIdleSeconds="120" timeToLiveSeconds="120" diskSpoolBufferSizeMB="30"
		maxEntriesLocalDisk="10000000" diskExpiryThreadIntervalSeconds="120"
		memoryStoreEvictionPolicy="LRU" statistics="true">
		<persistence strategy="localTempSwap" />
	</defaultCache>

	<cache name="employee" maxEntriesLocalHeap="10000" eternal="false"
		timeToIdleSeconds="5" timeToLiveSeconds="10">
		<persistence strategy="localTempSwap" />
	</cache>

	<cache name="org.hibernate.cache.internal.StandardQueryCache"
		maxEntriesLocalHeap="5" eternal="false" timeToLiveSeconds="120">
		<persistence strategy="localTempSwap" />
	</cache>

	<cache name="org.hibernate.cache.spi.UpdateTimestampsCache"
		maxEntriesLocalHeap="5000" eternal="true">
		<persistence strategy="localTempSwap" />
	</cache>
</ehcache>

Hibernate EHCache proporciona muchas opciones; no entraré en muchos detalles, pero algunas de las configuraciones importantes mencionadas anteriormente son:

  1. diskStore: EHCache almacena datos en la memoria, pero cuando comienza a desbordarse, empieza a escribir datos en el sistema de archivos. Utilizamos esta propiedad para definir la ubicación donde EHCache escribirá los datos desbordados.
  2. defaultCache: Es una configuración obligatoria, se utiliza cuando un objeto necesita ser almacenado en caché y no hay regiones de caché definidas para eso.
  3. nombre de caché=”empleado”: Utilizamos el elemento de caché para definir la región y sus configuraciones. Podemos definir múltiples regiones y sus propiedades, mientras definimos las propiedades de caché de los beans modelo, también podemos definir la región con estrategias de almacenamiento en caché. Las propiedades de caché son fáciles de entender y claras con el nombre.
  4. Las regiones de caché org.hibernate.cache.internal.StandardQueryCache y org.hibernate.cache.spi.UpdateTimestampsCache se definen porque EHCache estaba dando advertencias sobre eso.

Cache de segundo nivel de Hibernate – Estrategia de almacenamiento en caché de beans modelo

Utilizamos la anotación org.hibernate.annotations.Cache para proporcionar la configuración de caché. org.hibernate.annotations.CacheConcurrencyStrategy se utiliza para definir la estrategia de almacenamiento en caché y también podemos definir la región de caché a utilizar para los beans modelo.

package com.journaldev.hibernate.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;

import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter;

@Entity
@Table(name = "ADDRESS")
@Cache(usage=CacheConcurrencyStrategy.READ_ONLY, region="employee")
public class Address {

	@Id
	@Column(name = "emp_id", unique = true, nullable = false)
	@GeneratedValue(generator = "gen")
	@GenericGenerator(name = "gen", strategy = "foreign", 
				parameters = { @Parameter(name = "property", value = "employee") })
	private long id;

	@Column(name = "address_line1")
	private String addressLine1;

	@Column(name = "zipcode")
	private String zipcode;

	@Column(name = "city")
	private String city;

	@OneToOne
	@PrimaryKeyJoinColumn
	private Employee employee;

	public long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public String getAddressLine1() {
		return addressLine1;
	}

	public void setAddressLine1(String addressLine1) {
		this.addressLine1 = addressLine1;
	}

	public String getZipcode() {
		return zipcode;
	}

	public void setZipcode(String zipcode) {
		this.zipcode = zipcode;
	}

	public String getCity() {
		return city;
	}

	public void setCity(String city) {
		this.city = city;
	}

	public Employee getEmployee() {
		return employee;
	}

	public void setEmployee(Employee employee) {
		this.employee = employee;
	}

}
package com.journaldev.hibernate.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.Table;

import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.Cascade;

@Entity
@Table(name = "EMPLOYEE")
@Cache(usage=CacheConcurrencyStrategy.READ_ONLY, region="employee")
public class Employee {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "emp_id")
	private long id;

	@Column(name = "emp_name")
	private String name;

	@Column(name = "emp_salary")
	private double salary;

	@OneToOne(mappedBy = "employee")
	@Cascade(value = org.hibernate.annotations.CascadeType.ALL)
	private Address address;

	public long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public Address getAddress() {
		return address;
	}

	public void setAddress(Address address) {
		this.address = address;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public double getSalary() {
		return salary;
	}

	public void setSalary(double salary) {
		this.salary = salary;
	}

}

Ten en cuenta que estoy utilizando la misma configuración de base de datos que en el ejemplo de HQL en HQL, es posible que desees verificar eso para crear las tablas de la base de datos y cargar datos de muestra.

Clase de utilidad de Hibernate SessionFactory

Tenemos una clase de utilidad simple para configurar Hibernate y obtener la instancia única de SessionFactory.

package com.journaldev.hibernate.util;

import org.hibernate.SessionFactory;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.Configuration;
import org.hibernate.service.ServiceRegistry;

public class HibernateUtil {

	private static SessionFactory sessionFactory;
	
	private static SessionFactory buildSessionFactory() {
        try {
            // Crear la SessionFactory desde hibernate.cfg.xml
        	Configuration configuration = new Configuration();
        	configuration.configure("hibernate.cfg.xml");
        	System.out.println("Hibernate Configuration loaded");
        	
        	ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder().applySettings(configuration.getProperties()).build();
        	System.out.println("Hibernate serviceRegistry created");
        	
        	SessionFactory sessionFactory = configuration.buildSessionFactory(serviceRegistry);
        	
            return sessionFactory;
        }
        catch (Throwable ex) {
            System.err.println("Initial SessionFactory creation failed." + ex);
            ex.printStackTrace();
            throw new ExceptionInInitializerError(ex);
        }
    }
	
	public static SessionFactory getSessionFactory() {
		if(sessionFactory == null) sessionFactory = buildSessionFactory();
        return sessionFactory;
    }
}

Nuestro proyecto de caché de segundo nivel de Hibernate utilizando Hibernate EHCache está listo, escribamos un programa simple para probarlo.

Programa de prueba de Hibernate EHCache

package com.journaldev.hibernate.main;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.stat.Statistics;

import com.journaldev.hibernate.model.Employee;
import com.journaldev.hibernate.util.HibernateUtil;

public class HibernateEHCacheMain {

	public static void main(String[] args) {
		
		System.out.println("Temp Dir:"+System.getProperty("java.io.tmpdir"));
		
		// Inicializar sesiones
		SessionFactory sessionFactory = HibernateUtil.getSessionFactory();
		Statistics stats = sessionFactory.getStatistics();
		System.out.println("Stats enabled="+stats.isStatisticsEnabled());
		stats.setStatisticsEnabled(true);
		System.out.println("Stats enabled="+stats.isStatisticsEnabled());
		
		Session session = sessionFactory.openSession();
		Session otherSession = sessionFactory.openSession();
		Transaction transaction = session.beginTransaction();
		Transaction otherTransaction = otherSession.beginTransaction();
		
		printStats(stats, 0);
		
		Employee emp = (Employee) session.load(Employee.class, 1L);
		printData(emp, stats, 1);
		
		emp = (Employee) session.load(Employee.class, 1L);
		printData(emp, stats, 2);
		
		// Limpiar la caché de primer nivel para que se utilice la caché de segundo nivel
		session.evict(emp);
		emp = (Employee) session.load(Employee.class, 1L);
		printData(emp, stats, 3);
		
		emp = (Employee) session.load(Employee.class, 3L);
		printData(emp, stats, 4);
		
		emp = (Employee) otherSession.load(Employee.class, 1L);
		printData(emp, stats, 5);
		
		// Liberar recursos
		transaction.commit();
		otherTransaction.commit();
		sessionFactory.close();
	}

	private static void printStats(Statistics stats, int i) {
		System.out.println("***** " + i + " *****");
		System.out.println("Fetch Count="
				+ stats.getEntityFetchCount());
		System.out.println("Second Level Hit Count="
				+ stats.getSecondLevelCacheHitCount());
		System.out
				.println("Second Level Miss Count="
						+ stats
								.getSecondLevelCacheMissCount());
		System.out.println("Second Level Put Count="
				+ stats.getSecondLevelCachePutCount());
	}

	private static void printData(Employee emp, Statistics stats, int count) {
		System.out.println(count+":: Name="+emp.getName()+", Zipcode="+emp.getAddress().getZipcode());
		printStats(stats, count);
	}

}

org.hibernate.stat.Statistics proporciona las estadísticas de la SessionFactory de Hibernate. Lo estamos utilizando para imprimir el recuento de recuperación y las estadísticas de aciertos, fallos y colocaciones de la caché de segundo nivel. Las estadísticas están deshabilitadas por defecto para un mejor rendimiento, por eso las activo al inicio del programa. Cuando ejecutamos el programa anterior, obtenemos una gran cantidad de salida generada por las APIs de Hibernate y EHCache, pero estamos interesados en los datos que estamos imprimiendo. Una ejecución de muestra imprime la siguiente salida.

Temp Dir:/var/folders/h4/q73jjy0902g51wkw0w69c0600000gn/T/
Hibernate Configuration loaded
Hibernate serviceRegistry created
Stats enabled=false
Stats enabled=true
***** 0 *****
Fetch Count=0
Second Level Hit Count=0
Second Level Miss Count=0
Second Level Put Count=0
Hibernate: select employee0_.emp_id as emp_id1_1_0_, employee0_.emp_name as emp_name2_1_0_, employee0_.emp_salary as emp_sala3_1_0_, address1_.emp_id as emp_id1_0_1_, address1_.address_line1 as address_2_0_1_, address1_.city as city3_0_1_, address1_.zipcode as zipcode4_0_1_ from EMPLOYEE employee0_ left outer join ADDRESS address1_ on employee0_.emp_id=address1_.emp_id where employee0_.emp_id=?
1:: Name=Pankaj, Zipcode=95129
***** 1 *****
Fetch Count=1
Second Level Hit Count=0
Second Level Miss Count=1
Second Level Put Count=2
2:: Name=Pankaj, Zipcode=95129
***** 2 *****
Fetch Count=1
Second Level Hit Count=0
Second Level Miss Count=1
Second Level Put Count=2
3:: Name=Pankaj, Zipcode=95129
***** 3 *****
Fetch Count=1
Second Level Hit Count=2
Second Level Miss Count=1
Second Level Put Count=2
Hibernate: select employee0_.emp_id as emp_id1_1_0_, employee0_.emp_name as emp_name2_1_0_, employee0_.emp_salary as emp_sala3_1_0_, address1_.emp_id as emp_id1_0_1_, address1_.address_line1 as address_2_0_1_, address1_.city as city3_0_1_, address1_.zipcode as zipcode4_0_1_ from EMPLOYEE employee0_ left outer join ADDRESS address1_ on employee0_.emp_id=address1_.emp_id where employee0_.emp_id=?
4:: Name=Lisa, Zipcode=560100
***** 4 *****
Fetch Count=2
Second Level Hit Count=2
Second Level Miss Count=2
Second Level Put Count=4
5:: Name=Pankaj, Zipcode=95129
***** 5 *****
Fetch Count=2
Second Level Hit Count=4
Second Level Miss Count=2
Second Level Put Count=4

Como se puede ver en la salida, las estadísticas estaban deshabilitadas al principio, pero las activamos para verificar nuestra caché de segundo nivel de Hibernate. La explicación paso a paso de la salida es la siguiente:

  1. Antes de cargar cualquier dato en nuestra aplicación, todas las estadísticas son 0, como se esperaba.
  2. Cuando cargamos el empleado con id=1 por primera vez, primero se busca en la caché de primer nivel y luego en la caché de segundo nivel. Si no se encuentra en la caché, se ejecuta una consulta a la base de datos y, por lo tanto, el recuento de recuperación se convierte en 1. Una vez que el objeto se carga, se guarda tanto en la caché de primer nivel como en la de segundo nivel. Por lo tanto, el recuento de aciertos de nivel secundario sigue siendo 0 y el recuento de fallos se convierte en 1. Observe que el recuento de colocaciones es 2, porque el objeto Employee consta también de Address, por lo que ambos objetos se guardan en la caché de segundo nivel y el recuento se incrementa a 2.
  3. A continuación, volvemos a cargar el empleado con id=1, esta vez está presente en la caché de primer nivel. Por lo tanto, no se ve ninguna consulta a la base de datos y todas las demás estadísticas de la caché de segundo nivel también permanecen iguales.
  4. A continuación, estamos utilizando el método evict() para eliminar el objeto de empleado de la caché de primer nivel. Ahora, cuando intentamos cargarlo, Hibernate lo encuentra en la caché de segundo nivel. Por eso no se realiza ninguna consulta a la base de datos y el recuento de recuperación permanece en 1. Observa que el recuento de aciertos pasa de 0 a 2 porque tanto los objetos Employee como Address se leen desde la caché de segundo nivel. El recuento de fallos de segundo nivel y el recuento de inserciones permanecen en el valor anterior.
  5. A continuación, cargamos un empleado con id=3, se ejecuta una consulta a la base de datos y el recuento de recuperación aumenta a 2, el recuento de fallos aumenta de 1 a 2 y el recuento de inserciones aumenta de 2 a 4.
  6. A continuación, intentamos cargar un empleado con id=1 en otra sesión. Dado que la caché de segundo nivel de Hibernate se comparte entre sesiones, se encuentra en la caché de segundo nivel y no se ejecuta ninguna consulta a la base de datos. El recuento de recuperación, el recuento de fallos y el recuento de inserciones permanecen iguales, mientras que el recuento de aciertos aumenta de 2 a 4.

Por lo tanto, está claro que nuestra caché de segundo nivel de Hibernate; Hibernate EHCache; está funcionando correctamente. Las estadísticas de Hibernate son útiles para encontrar el cuello de botella en el sistema y optimizarlo para reducir el recuento de recuperación y cargar más datos desde la caché. Eso es todo para el ejemplo de Hibernate EHCache, espero que te ayude a configurar EHCache en tus aplicaciones de Hibernate y a obtener un mejor rendimiento a través de la caché de segundo nivel de Hibernate. Puedes descargar el proyecto de muestra desde el siguiente enlace y usar otros datos estadísticos para aprender más.

Descargar Proyecto Hibernate EHCache

Source:
https://www.digitalocean.com/community/tutorials/hibernate-ehcache-hibernate-second-level-cache