Persistencia en BD con Spring: Integrando JPA, c3p0, Hibernate y EHCache


VERSIONES

  1. 19/06/2013 (Primera publicación)
  2. 06/07/2013:
    • Se cambia la dependencia hibernate-c3p0 por la de c3p0 ya que la conexión a la base de datos se define en Spring y no se utiliza la integración con Hibernate
    • Se amplia la sección dedicada a EHCache
  3. 28/09/2013:Añadido cómo acceder a sesión de Hibernate tanto al artículo como a la demo. Se han actualizado las librerias empleadas.
  4. 24/12/2013:Uso programático de la caché. Se han actualizado las librerias empleadas.
  5. 27/09/2014:Se elimina el persistence.xml. Revisión de la redacción. Se han actualizado las librerias empleadas.
  6. 08/11/2014:uso de JUnit
  7. 15/02/2015:propiedades de c3p0

logo spring

 Una de las fortalezas de Spring es que no es un simple framework al uso, sino una suerte de metaframework o ecosistema compuesto por multitud de módulos conectados entre sí pero también integrables con frameworks externos o estándares JEE. El núcleo de Spring (Spring IoC Container) puede hacer de “pegamento” en nuestros proyectos entre las distintas tecnologías que usemos pertenezcan o no al propio Spring. Por ejemplo, que Spring incluya un módulo MVC para la capa de vista de aplicaciones web (SpringMVC) no es excluyente para que usemos cualquier otro framework de vista como Struts2, JSF, GWT…

En este blog ya hemos atisbado estas posibilidades de integración con el artículo sobre servicios web, donde usamos el core de Spring para una aplicación web que ofrecía servicios SOAP a través del estándar JAX-WS y usando como implementación un framework de terceros como Apache CXF. En esta ocasión, se verá cómo realizar una integración que probablemente realicemos en la mayoría de nuestros proyectos Spring: la de SpringIoC con un framework ORM para gestionar con Spring la persistencia, incluyendo la transaccionalidad.

Así pues, se va a mostrar una integración que he realizado en numerosas ocasiones en mis proyectos y que forma parte de uno de los “stack” de desarrollo web Java más utilizados: Spring y JPA con la implementación de Hibernate, probablemente el ORM más popular para Java. También se usará el pool de conexiones c3p0 para optimizar la conexión con la base de datos, que en este artículo será MySQl, y veremos la configuración y uso básico de la caché de objetos EHCache para mejorar el rendimiento de la aplicación.

Nota: En este ejemplo se va a utilizar un DAO para implementar a través de JPA\Hibernate las operaciones con la BD, pero recomiendo encarecidamente el uso de Spring Data JPA para estandarizar, simplificar y acelerar el desarrollo de la capa de persistencia de nuestras aplicaciones Java.

Entorno de pruebas:

Requisitos: Conocimientos básicos de Maven, JPA y Spring IoC. Recomendable JUnit.

Proyecto para pruebas

Para las pruebas se usará un proyecto Maven no web, la base de este proyecto será el siguiente pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0    	http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.danielme.demo</groupId>
	<artifactId>spring-jpa-hibernate</artifactId>
	<version>1.0</version>
	<name>spring-jpa-hibernate</name>
	<inceptionYear>2013</inceptionYear>

	<description>Spring 3 + JPA 2 + Hibernate 4 + c3p0 + EHCache + log4j Integration Demo</description>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.build.mainClass>com.danielme.demo.springjpahib.Main</project.build.mainClass>
		<java.version>1.6</java.version>
		<spring.version>3.2.11.RELEASE</spring.version>
		<hibernate.version>4.3.6.Final</hibernate.version>
		<c3p0.version>0.9.1.2</c3p0.version>
		<log4j.version>1.2.17</log4j.version>
		<mysql.driver.version>5.1.33</mysql.driver.version>
		<junit.version>4.11</junit.version>
		<maven.shade.plugin>2.3</maven.shade.plugin>
		<maven.exec.plugin>1.3.2</maven.exec.plugin>
		<maven.compiler.plugin.version>3.1</maven.compiler.plugin.version>
		<maven.jar.plugin.version>2.5</maven.jar.plugin.version>
	</properties>

	<prerequisites>
		<maven>3.0</maven>
	</prerequisites>


	<developers>
		<developer>
			<id>dmedina</id>
			<name>Daniel Medina</name>
			<email>danielme_com@yahoo.com</email>
			<url>http://danielme.com</url>
			<roles>
				<role>developer</role>
			</roles>
		</developer>
	</developers>

	<scm>
		<url>https://github.com/danielme-com/Spring-3---JPA-2---Hibernate-4---c3p0---EHCache---log4j-Integration-Demo.git</url>
	</scm>

	<licenses>
		<license>
			<name>GPL 3</name>
			<url>http://www.gnu.org/licenses/gpl-3.0.html</url>
		</license>
	</licenses>

	<repositories>

		<repository>
			<id>JBoss repository</id>
			<url>http://repository.jboss.org/nexus/content/groups/public/</url>
		</repository>

		<repository>
			<id>java.net-Public</id>
			<url>https://maven.java.net/content/groups/public/</url>
		</repository>

	</repositories>


	<build>
		<pluginManagement>
			<plugins>
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-jar-plugin</artifactId>
					<version>${maven.jar.plugin.version}</version>
					<configuration>
						<archive>
							<manifest>
								<addClasspath>true</addClasspath>
								<mainClass>${project.build.mainClass}</mainClass>
							</manifest>
						</archive>
					</configuration>
				</plugin>
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-compiler-plugin</artifactId>
					<version>${maven.compiler.plugin.version}</version>
					<configuration>
						<source>${java.version}</source>
						<target>${java.version}</target>
						<encoding>${project.build.sourceEncoding}</encoding>
					</configuration>
				</plugin>
				<!-- mvn clean package exec:java -->
				<plugin>
					<groupId>org.codehaus.mojo</groupId>
					<artifactId>exec-maven-plugin</artifactId>
					<version>${maven.exec.plugin}</version>
					<executions>
						<execution>
							<goals>
								<goal>java</goal>
							</goals>
						</execution>
					</executions>
					<configuration>
						<mainClass>${project.build.mainClass}</mainClass>
					</configuration>
				</plugin>
				<!-- package all dependencies in one executable big jar > mvn clean package 
					shade:shade -->
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-shade-plugin</artifactId>
					<version>${maven.shade.plugin}</version>
					<executions>
						<execution>
							<phase>package</phase>
							<goals>
								<goal>shade</goal>
							</goals>
						</execution>
					</executions>
					<configuration>
						<finalName>${project.artifactId}-${project.version}-all-deps</finalName>
						<transformers>
							<transformer
								implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
								<mainClass>${project.build.mainClass}</mainClass>
							</transformer>
						</transformers>
						<!-- http://zhentao-li.blogspot.com.es/2012/06/maven-shade-plugin-invalid-signature.html -->
						<filters>
							<filter>
								<artifact>*:*</artifact>
								<excludes>
									<exclude>META-INF/*.SF</exclude>
									<exclude>META-INF/*.DSA</exclude>
									<exclude>META-INF/*.RSA</exclude>
								</excludes>
							</filter>
						</filters>

					</configuration>
				</plugin>
			</plugins>
		</pluginManagement>
	</build>

	<dependencies>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>${spring.version}</version>
		</dependency>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-orm</artifactId>
			<version>${spring.version}</version>
		</dependency>

		<!--@Transactional -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-tx</artifactId>
			<version>${spring.version}</version>
		</dependency>

		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-entitymanager</artifactId>
			<version>${hibernate.version}</version>
		</dependency>

		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-ehcache</artifactId>
			<version>${hibernate.version}</version>
		</dependency>

		<!-- connection pooling with c3p0 -->
		<dependency>
			<groupId>c3p0</groupId>
			<artifactId>c3p0</artifactId>
			<version>${c3p0.version}</version>
		</dependency>

		<dependency>
			<groupId>log4j</groupId>
			<artifactId>log4j</artifactId>
			<version>${log4j.version}</version>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>${mysql.driver.version}</version>
		</dependency>
		
		<!-- testing -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>${junit.version}</version>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-test</artifactId>
			<version>${spring.version}</version>
			<scope>test</scope>
		</dependency>

	</dependencies>

</project>


Estas son las dependencias que necesitamos:

  • spring-context: es el “corazón” de Spring. Si estuviéramos en un proyecto web, la dependencia que deberíamos importar es spring-web
  • spring-orm: como su nombre indica, es necesario para usar un orm desde Spring.
  • spring-tx: nos permitirá gestionar las transacciones, en nuestro caso a golpe de anotaciones🙂
  • hibernate-entitymanager:Hibernate con soporte para JPA 2.1.
  • c3p0: pool de conexiones.
  • hibernate-ehcache: lo veremos al final del artículo.
  • mysql-connector-java: driver JDBC para MySQL/MariaDB. Es la base de datos que voy a utilizar tal y como se indica en el entorno de pruebas.
  • log4j: lo usaremos para que ver qué pasa…
  • JUnit y spring-test: el código de probará con tst de JUnit integrados con Spring.

La configuración para log4j (/src/main/resources/log4j) en modo debug permitirá comprobar que todo se inicializa correctamente, la apertura y cierre de transacciones, la utilización de la caché, etc…

	
<?xml version="1.0" encoding="UTF-8" ?>
 
<!DOCTYPE log4j:configuration PUBLIC
  "-//APACHE//DTD LOG4J 1.2//EN" "http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd">
 
<!-- http://wiki.apache.org/logging-log4j/Log4jXmlFormat -->
 
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
     
    <appender name="console" class="org.apache.log4j.ConsoleAppender">
        <param name="Target" value="System.out" />
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d{MM-dd-yyyy hh:mm:ss,SSS a} %5p %c{1}:%L - %m%n" />
        </layout>
    </appender>
 
    <root>
        <priority value="debug" />
        <appender-ref ref="console" />
    </root>
 
</log4j:configuration>

El applicationContext.xml

Vamos a proceder a encajar y configurar todas las piezas del puzzle en el fichero /src/main/resources/applicationContext.xml. Lo haremos paso a paso:

  1. Se habilita el uso de anotaciones para la definción de beans, inyección de dependencias, transacciones…
    	
    	<!-- Autowired -->
    	<!--  used to activate annotations in beans already registered in the application context (no matter if they were defined with XML or by package scanning) -->
    	<context:annotation-config />	
    	<!-- scans packages to find and register beans within the application context. -->
    	<context:component-scan base-package="com.danielme" />
        
            <!-- enable @Transactional Annotation -->    
    	<tx:annotation-driven transaction-manager="transactionManager" />
    	
    	<!-- @PersistenceUnit annotation -->
    	<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
    

    El transactionManager lo definiremos al final.

  2. Incluímos los .properties de configuración que utilizaremos para centralizar los parámetros de configuración y no tenerlos repartidos por los xml de Spring. En este ejemplo se utilizará el fichero db.properties con todos los parámetros relativos a la persistencia en la base de datos. Para el uso de properties en Spring, consultar el artículo al respecto.
    	
    	<context:property-placeholder location="classpath:db.properties" />
    
  3. Definimos el dataSource, esto es, el bean que usaremos para conectarnos a la base de datos y que por lo tanto requiere los datos de conexión a la misma. Tenemos varias opciones, como usar las implementaciones que incluye Spring (por ejemplo org.springframework.jdbc.datasource.DriverManagerDataSource) o soluciones de teceros como Apache DBCP. Habitualmente me decanto por c3p0 que ofrece un pool de conexiones de alto rendimiento. A grandes rasgos un pool de conexiones consiste en mantener un cierto número de conexiones abiertas con la base da datos con el objetivo de no tener que estar constantemente abriendo y cerrando conexiones lo que puede penalizar notablemente el rendimiento de la aplicación especialmente en una aplicación web. Por norma general, un pool de conexiones mantendrá siempre abierto un mínimo de conexiones, y el resto que se vayan abriendo permanecerán abiertas y disponibles en el pool durante un tiempo prudencial siendo liberadas si no usan en ese periodo de tiempo.

    Veamos la configuración de ejemplo:

    	
    	<!-- data source with c3p0 -->
    	<bean id="dataSource" class = "com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"
            p:driverClass="${jdbc.driverClassName}"
            p:jdbcUrl="${jdbc.url}"
            p:user="${jdbc.username}"
            p:password="${jdbc.password}"
            p:acquireIncrement="${c3p0.acquire_increment}"
            p:minPoolSize="${c3p0.min_size}"
            p:maxPoolSize="${c3p0.max_size}" 
            p:maxIdleTime="${c3p0.max_idle_time}"
            p:unreturnedConnectionTimeout="${c3p0.unreturned_connection_timeout}"/>
    

    Como ya se ha dicho, los parámetros de configuración de la bd y la persistencia están en el siguiente .properties:

    #connection
    jdbc.driverClassName = org.gjt.mm.mysql.Driver
    jdbc.username=user
    jdbc.password=password
    jdbc.url = jdbc:mysql://localhost:3306/demo
    
    #jpa
    jpa.database=MYSQL
    jpa.showSql=true
    
    #hibernate
    hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
    hibernate.hbm2ddl.auto= update
    hibernate.connection.autocommit = true
    hibernate.cache = true
    hibernate.query.cache = true
    hibernate.statistics = true
    
    #c3p0 pool
    c3p0.acquire_increment=5
    c3p0.max_size=100
    c3p0.min_size=5
    c3p0.max_idle_time=1200
    c3p0.unreturned_connection_timeout=600
    

    Con respecto a la configuración de c3p0, ésta admite más opciones que pueden consultarse en la documentación. Para cada aplicación habrá estudiar y determinar la configuración más apropiada. Los valores que se han definido en el ejemplo son los siguientes:

    • maxPoolSize: número máximo de conexiones que puede albergar el pool. Tener siempre en cuenta el límite configurado en la BD. Valor por defecto 15.
    • minPoolSize: número mínimo de conexiones que tendrá siempre el pool. Valor por defecto 3.
    • acquireIncrement: número de conexiones que solicitará a la vez el pool cuando necesite más conexiones. Es más eficiente traerlas en bloques de tamaño razonable que de una en una. Valor por defecto 3.
    • maxIdleTime: tiempo máximo en segundos que puede permancer una conexión en el pool sin ser utilizada. Valor por defecto 0 (infinito).
    • unreturnedConnectionTimeout: tiempo máximo en segundos que se le permite a la aplicación utilizar una conexión. Si se sobrepasa, c3p0 cierra esa conexión por lo que si todavía está en uso la aplicación fallará. El objetivo de este parámetro es impedir que la aplicación solicite conexiones y nunca las devuelva al pool por errores de programación (por ejemplo una transacción que no se finaliza). Generalmente este parámetro sólo se utilizará para depuración para asegurar que la aplicación no deja conexiones sin uso sin retornar al pool. Valor por defecto 0 (infinito).
  4. Definimos Hibernate como provider o implementación de JPA a utilizar
    	
         <!-- Hibernate as JPA vendor-->
    	<bean id="jpaAdapter"
    		class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"
    		p:database="${jpa.database}" p:showSql="${jpa.showSql}" />
    
  5. Ahora definimos el EntityManager de JPA, el “alma” de nuestro sistema de persistencia, configurando una factoría de Spring. En este bean definiremos algunos parámetros que cuando usamos directamente JPA sin Spring (véase el ejemplo de este artículo) se definen en el fichero /META-INF/persistence.xml.
    	
    	<bean id="entityManagerFactory"
    		class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
    		p:dataSource-ref="dataSource" p:packagesToScan="com.danielme.demo.springjpahib"
    		p:jpaVendorAdapter-ref="jpaAdapter">
    
    		<property name="jpaProperties">
    			<props>
    				<prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>
    				<prop key="hibernate.dialect">${hibernate.dialect}</prop>
    				<prop key="hibernate.connection.autocommit">${hibernate.connection.autocommit}</prop>
    				<prop key="hibernate.cache.use_second_level_cache">${hibernate.cache}</prop>
                                    <prop key="hibernate.cache.use_query_cache">${hibernate.query.cache}</prop>
                                    <prop key="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider</prop>
                    <prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</prop>
                    <!--useful for debugging-->
                    <prop key="hibernate.generate_statistics">${hibernate.statistics}</prop>
    			</props>
    		</property>
    
    		<property name="loadTimeWeaver">
    			<bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
    		</property>
    		
    	</bean>
    
  6. Por último, definimos un gestor de transacciones para nuestro EntityManager, bean al que ya hicimos referencia al principio de esta sección. Las transacciones en Spring se hacen mediante aspectos pero su uso ha sido simplificado al máximo con la anotación @Transactional.
    	
    	<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"
    		p:entityManagerFactory-ref="entityManagerFactory" />
    

Resumiendo, así nos ha quedado el fichero el cual podemos reutilizar fácilmente a modo de plantilla:

	
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
	xmlns:tx="http://www.springframework.org/schema/tx" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans  http://www.springframework.org/schema/beans/spring-beans-3.2.xsd  http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context-3.2.xsd  http://www.springframework.org/schema/tx  http://www.springframework.org/schema/tx/spring-tx-3.2.xsd  http://www.springframework.org/schema/aop  http://www.springframework.org/schema/aop/spring-aop-3.2.xsd">

	<!-- Autowired -->
	<!--  used to activate annotations in beans already registered in the application context (no matter if they were defined with XML or by package scanning) -->
	<context:annotation-config />	
	<!-- scans packages to find and register beans within the application context. -->
	<context:component-scan base-package="com.danielme" />
    
    <!-- enable @Transactional Annotation -->    
	<tx:annotation-driven transaction-manager="transactionManager" />
	
	<!-- @PersistenceUnit annotation -->
	<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
   
	<!--  classpath*:*.properties-->
	<context:property-placeholder location="classpath:db.properties" />			
		
	<!-- data source with c3p0 -->
	<bean id="dataSource" class = "com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"
        p:driverClass="${jdbc.driverClassName}"
        p:jdbcUrl="${jdbc.url}"
        p:user="${jdbc.username}"
        p:password="${jdbc.password}"
        p:acquireIncrement="${c3p0.acquire_increment}"
        p:minPoolSize="${c3p0.min_size}"
        p:maxPoolSize="${c3p0.max_size}" 
        p:maxIdleTime="${c3p0.max_idle_time}"
        p:unreturnedConnectionTimeout="${c3p0.unreturned_connection_timeout}"/>
 	

     <!-- Hibernate as JPA vendor-->
	<bean id="jpaAdapter"
		class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"
		p:database="${jpa.database}" p:showSql="${jpa.showSql}" />

	<bean id="entityManagerFactory"
		class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
		p:dataSource-ref="dataSource" p:packagesToScan="com.danielme.demo.springjpahib"
		p:jpaVendorAdapter-ref="jpaAdapter">

		<property name="jpaProperties">
			<props>
				<prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>
				<prop key="hibernate.dialect">${hibernate.dialect}</prop>
				<prop key="hibernate.connection.autocommit">${hibernate.connection.autocommit}</prop>
				<prop key="hibernate.cache.use_second_level_cache">${hibernate.cache}</prop>
                <prop key="hibernate.cache.use_query_cache">${hibernate.query.cache}</prop>
                <prop key="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider</prop>
                <prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</prop>
                <!--useful for debugging-->
                <prop key="hibernate.generate_statistics">${hibernate.statistics}</prop>
			</props>
		</property>

		<property name="loadTimeWeaver">
			<bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
		</property>

		
	</bean>

	<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"
		p:entityManagerFactory-ref="entityManagerFactory" />

</beans>

Desde Spring 3.1 el archivo persistence.xml es innecesario ya que la configuración que se realiza en el mismo se puede definir directamente en el LocalContainerEntityManagerFactoryBean. Si no usamos este fichero, como en el presente ejemplo, tendremos que indicar las entidades de JPA que tengamos anotadas y se quieran utilizar mediante la propiedad “packagesToScan”. En el caso de que se hiciera uso de mencionado fichero, habría que definir la propiedad persistenceXmlLocation e indicar el persistenceUnitName a utilizar del siguiente modo:

	
<bean id="entityManagerFactory"
		class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
		p:dataSource-ref="dataSource" p:jpaVendorAdapter-ref="jpaAdapter"
                p:persistenceXmlLocation="classpath:/META-INF/persistence.xml">

		<property name="persistenceUnitName" value="danielme_persistenceunit"/>

		<property name="jpaProperties">

Entidad y clase DAO de ejemplo

La integración que prometía el artículo ya está realizada y ahora se probará directamente con un DAO (no habrá capa de servicio o negocio) y su entidad con MySQL.

La entidad es la siguiente clase Country. Al crearse un objeto se guardará automáticamente un timestamp gracias al método anotado con @PrePersist, comportamiento habitual para realizar una auditoria de las operaciones realizadas. En el artículo a Persistencia en BD con Spring Data JPA (III): Auditoría veremos una solución alternativa.

package com.danielme.demo.springjpahib;

import java.util.Calendar;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.PrePersist;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@Entity
@Table(name="countries")
public class Country
{
	 @Id
	 @GeneratedValue(strategy = GenerationType.AUTO)
	 private Long id;
	 
	 @Column(nullable = false, unique=true)
	 private String name;
	 
	 @Column(nullable = false)
	 private Integer population;
	 
	 @Column(updatable = false, nullable = false)
	 @Temporal(TemporalType.TIMESTAMP)
	 private Calendar creation;
	 
	 public Country()
	 {
		super();
	 }

	public Country(String name, Integer population)
	{
		super();
		this.name = name;
		this.population = population;
	}
	
	 @PrePersist
	 public void onPersist()
	 {
		 creation = Calendar.getInstance();
	 }

	public Long getId()
	{
		return id;
	}

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

	public String getName()
	{
		return name;
	}

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

	public Integer getPopulation()
	{
		return population;
	}

	public void setPopulation(Integer population)
	{
		this.population = population;
	}

	public Calendar getCreation()
	{
		return creation;
	}

	public void setCreation(Calendar creation)
	{
		this.creation = creation;
	}

}

Y ahora la clase DAO, muy sencilla ya que se trata de un ejemplo para pruebas. Seguimos la buena práctica de definir primero una interfaz

	
package com.danielme.demo.springjpahib;

import java.util.List;

public interface CountryDao
{
	
	  void addAll(List<Country> list);
	 
	  Country getCountryByName(String name);
	  
	  void deleteAll();

          List<Country> getAll();

	  Country getById(Long id);	

}

Su implementación es un bean de Spring anotado con @Repository por ser un DAO. El EntityManager se inyecta con la anotación @PersistenceContext (esta inyección no puede realizarse a través de constructor al menos con la versión de Spring empleada) y las transacciones se indican con @Transactional tal y como ya se ha comentado. Esta última anotación tiene varias opciones para configurar la transaccionalidad, aunque se usarán los valores por defecto. El DAO incluye un ejemplo de uso de la API Criteria de JPA (personalmente, y no soy el único, encuentro esta API muy complicada y siempre me he sentido más cómodo utilizando directamente JPQL).

	
package com.danielme.demo.springjpahib;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;

import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;


@Repository
public class CountryDaoImpl implements CountryDao
{
	@PersistenceContext
	private EntityManager entityManager;

	@Override
	@Transactional
	public void addAll(List<Country> list)
	{
		for(Country country : list)
		{
			entityManager.persist(country);
		}		
	}

	@Override
	public Country getCountryByName(String name)
	{
	  CriteriaBuilder cb = entityManager.getCriteriaBuilder();
	  CriteriaQuery<Country> query = cb.createQuery(Country.class);

	  Root<Country> country = query.from(Country.class);
	  query.where(cb.equal(country.<String>get("name"),
	  cb.parameter(String.class, "param")));

	  TypedQuery<Country> tq = entityManager.createQuery(query);
	  tq.setParameter("param", name);
	  return tq.getResultList().get(0);
	}

	@Override
	@Transactional
	public void deleteAll()
	{
	     entityManager.createQuery("DELETE FROM Country").executeUpdate();		
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<Country> getAll()
	{
		Query query = entityManager.createQuery("from " + Country.class.getName());
		return query.getResultList();
	}

	@Override
	public Country getById(Long id)
	{
		return entityManager.find(Country.class, id);
	}		

}

Probando con JUnit

Ahora toca probar el DAO y comprobar que la integración de Spring e Hibernate funciona correctamente. Para ello utilizaremos tests de JUnit 4 definidos en la clase com.danielme.springjpahib.test.CountryDaoTest dentro del directorio /src/test/java. Esta clase debe anotarse con @RunWith(SpringJUnit4ClassRunner.class)
y @ContextConfiguration(“file:src/main/resources/applicationContext.xml”)
para que al instanciarse inicie Spring y además se puedan inyectar sus beans. Asimismo, en las clases con tests que interactuen con bases de datos es una práctica habitual cargar en la mismas un juego de datos inicial sobre el que se realizarán las pruebas; en nuestro caso el método setUp() anotado con @Before se ejecutará siempre antes de cualquier test lo que garantizará que en la BD de pruebas se tengan siempre los mismos datos al iniciarse cada test.

Los métodos en los que se implementarán los test se anotarán con @Test y se comprobará la validez de las operaciones que se vayan testeando con los métodos estáticos de org.junit.Assert (por comodidad es una práctica habitual definir estos métodos mediante import estáticos). Generalmente se creará un método test para cada funcionalidad (p.ej un método de un servicio) a testear, aunque en el ejemplo debido a su sencillez sólo se creará uno.

	
package com.danielme.springjpahib.test;

import static org.junit.Assert.assertEquals;

import java.util.LinkedList;
import java.util.List;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

import com.danielme.demo.springjpahib.Country;
import com.danielme.demo.springjpahib.CountryDao;

/**
 * Some test cases.
 * 
 * @author danielme.com
 * 
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/resources/applicationContext.xml")
public class CountryDaoTest
{

	@Autowired
	private CountryDao countryDao;

	@Before
	@Transactional
	public void setUp() throws Exception
	{
		// empty repository
		countryDao.deleteAll();

		List<Country> countries = new LinkedList<Country>();
		countries.add(new Country("Spain", 47265321));
		countries.add(new Country("Mexico", 115296767));
		countries.add(new Country("Germany", 81799600));

		countryDao.addAll(countries);
	}

	@Test
	public void getMethodsTest()
	{
		assertEquals(countryDao.getAll().size(), 3);
		Country country = countryDao.getCountryByName("Spain");
		assertEquals(country.getName(), "Spain");
		assertEquals(countryDao.getById(country.getId()).getName(), "Spain");
	}

}

Si ejecutamos esta clase con Maven (mvn test) o en un IDE y revisamos el log, podemos comprobar la inicialización de c3p0, la gestión de las conexiones, las transacciones, las consultas ejecutadas, etc.

Hibernate y EHCache

Nota: lo que se va a exponer en esta sección son funcionalidades específicas de Hibernate y que por tanto están fuera del estándar JPA.

EHCache es una caché de objetos bastante potente y popular (por ejemplo es usada en Alfresco) que permite “cachear” objetos en memoria o disco. Se integra perfectamente con Hibernate para mejorar el rendimiento de nuestra aplicación al evitarse en numerosas ocasiones la realización de las consultas a base de datos. También se puede integrar con Spring para cachear los resultados de los métodos de servicio, aunque esto daría ya para otro artículo.

En realidad, EHCache no es más que una de las cachés que Hibernate puede utilizar como caché de segundo nivel. La caché de primer nivel de Hibernate se corresponde con cada sesión abierta con la base de datos, mientras que la caché de segundo nivel está desactivada por defecto y es común a todas las sesiones, es decir, a todo el entorno de persistencia.

Habilitar la caché de segundo nivel en Hibernate utilizando EHCache como proveedor es sencilo:

  1. Incorporar en el pom la siguiente dependencia (ya estaba definida en el pom de ejemplo):

    <dependency>
    			<groupId>org.hibernate</groupId>
    			<artifactId>hibernate-ehcache</artifactId>
    			<version>${hibernate.version}</version>
    		</dependency>		
              
    
  2. Se activa la caché de segundo nivel, la caché de consultas y se indica el proveedor a utilizar:

    <prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>
    <prop key="hibernate.dialect">${hibernate.dialect}</prop>
    <prop key="hibernate.connection.autocommit">${hibernate.connection.autocommit}</prop>
    <prop key="hibernate.cache.use_second_level_cache">${hibernate.cache}</prop>
    <prop key="hibernate.cache.use_query_cache">${hibernate.query.cache}</prop>
    <prop key="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider</prop>
    <prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</prop>
    <!--useful for debugging-->
    <prop key="hibernate.generate_statistics">${hibernate.statistics}</prop>
    

Ya está lista toda la configuración. Ahora simplemente hay que anotar aquellas entidades que queramos que sean cacheadas.

@org.hibernate.annotations.Cache(usage = org.hibernate.annotations.CacheConcurrencyStrategy.READ_WRITE)
@Entity
@Table(name="countries")
public class Country
{

La anotación @Cache también puede ser aplicada a las relaciones de la entidad.

Para cachear el resultado de consultas usaremos la siguiente sentencia.

	@SuppressWarnings("unchecked")
	@Override
	public List<Country> getAll()
	{
		Query query = entityManager.createQuery("from " + Country.class.getName());
		query.setHint(QueryHints.HINT_CACHEABLE, true);
		return query.getResultList();
	}	

Se puede comprobar fácilmente que la caché funciona correctamente revisando el log (está configurado en modo DEBUG) y ver cómo su uso evita llamadas a la base de datos si los datos solicitados están cacheados. Por ejemplo, hagamos dos llamadas consecutivas realizando a getAll:

 countryDao.getAll();
 countryDao.getAll();

En el log veremos que para la primera llamada se realiza la consulta a la BD mientras que para la segunda llamada veremos una línea tal que así:

Returning cached query results

Asimismo, la entidad Country está anotada como “cacheable”. Esto implica que cuando se recupera de la base de datos es almacenada en EHCache y que cuando solicita explícitamente un objeto de esta entidad se obtendrá de la caché si es que se encuentra en ella. Cuando digo “explícitamente” me refiero a obtenerla directamente a través de JPA/Hibernate y no mediante una query ya que en el segundo caso no hay manera de saber si los resultados que debe devolver esa query están todos cacheados o no (de ahí que se cacheen los ResulSet de las Querys). Podemos comprobarlo simplemente añadiendo este código de prueba y revisando el log.

  		Country country = countryDao.getCountryByName("Spain");
  		countryDao.getById(country.getId()); 

La caché es gestionada por la aplicación lo que implica que si se modifican datos cacheados fuera de esta (por ejemplo modificamos “a mano” los registros de la base de datos u otra aplicación modifica la misma) la caché no puede tener conocimiento de estos cambios y nos devolverá los datos obsoletos (esto es, los existentes en la caché y no los actuales en la base de datos).

A la hora de usar cachés debemos ser muy cautelosos ya que podemos terminar con demasiados objetos guardados en memoria (aunque la duración máxima de estos objetos en la caché con y/o sin uso es configurable con las propiedades timeToLive y timeToIdle) por lo que la esperada mejora de rendimiento puede terminar volviéndose en nuestra contra. Sólo deberíamos cachear aquellas consultas\entidades que sean muy recurrentes y cuyos resultados no suelan cambiar. Si no sabemos lo que estamos haciendo y comprobamos que nuestra configuración de la caché verdaderamente mejora el rendimiento (por ejemplo haciendo pruebas de carga con JMeter), lo mejor es simplemente no usarla.

Nota:Se puede utilizar Metrics para monitorizar la utilización de EHCache.

EHCache es muy configurable, incluso a nivel de entidad, aunque en este artículo no vamos entrar en detalles. Lo más sencillo es definir esta configuración en el fichero /src/main/resources/ehcache.xml. Un ejemplo típico:

<?xml version="1.0" encoding="UTF-8" ?>

<ehcache>
    <defaultCache
        maxElementsInMemory="5000"
        eternal="false"
        timeToIdleSeconds="120"
        timeToLiveSeconds="120"
        overflowToDisk="false"/>

    <!-- settings per class -->
    <cache name="com.danielme.demo.springjpahib.Country"
        maxElementsInMemory="1000"
        eternal="false"
        timeToIdleSeconds="180"
        timeToLiveSeconds="180"
        overflowToDisk="false"/>
</ehcache>

Podemos tomar control de la caché de forma programática añadiendo por ejemplo los siguientes métodos al DAO

@Override
	@Transactional
	public void clearEntityCache() 
	{
		SessionFactory sessionFactory = entityManager.unwrap(Session.class).getSessionFactory();
		sessionFactory.getCache().evictEntityRegion(Country.class);
	}

	@Override
	@Transactional
	public void clearEntityFromCache(Long id) {
		SessionFactory sessionFactory = entityManager.unwrap(Session.class).getSessionFactory();
		sessionFactory.getCache().evictEntity(Country.class, id);
	}

	@Override
	@Transactional
	public void clearHibenateCache() {
		SessionFactory sessionFactory = entityManager.unwrap(Session.class).getSessionFactory();
		sessionFactory.getCache().evictEntityRegions();
		sessionFactory.getCache().evictCollectionRegions();
		sessionFactory.getCache().evictDefaultQueryRegion();
		sessionFactory.getCache().evictQueryRegions();
	}

Para comprobar el funcionamiento de la caché y los últimos métodos incorporados al DAO se ha creado un nuevo test:

        @Test
	public void cacheTest()
	{
		assertEquals(countryDao.getAll().size(), 3);
		// check the log in debug mode: "Returning cached query results"
		assertEquals(countryDao.getAll().size(), 3);

		Country country = countryDao.getCountryByName("Spain");
		// get the country from the cache because this entity is annotated with
		// @org.hibernate.annotations.Cache
		assertEquals(countryDao.getById(country.getId()).getName(), "Spain");

		countryDao.clearEntityFromCache(country.getId());
		// this time the entity is retrieved from the db
		assertEquals(countryDao.getById(country.getId()).getName(), "Spain");
	}

Acceso a Hibernate Session desde JPA

Por último, un pequeño tip para poder acceder a Hibernate Session cuando trabajamos con JPA 2 lo cual nos permite por ejemplo utilizar la API Criteria de Hibernate. Lo único que debemos hacer es invocar el método unwrap. En JPA 1 se usaba el método getDelegate el cual podría resultar problemático.

	  Session session = entityManager.unwrap(Session.class);
	  Criteria criteria = session.createCriteria(Country.class);
	  criteria.add(Restrictions.eq("name", name));
	  return (Country) criteria.uniqueResult();

Código de ejemplo

Se encuentra disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.

4 Responses to Persistencia en BD con Spring: Integrando JPA, c3p0, Hibernate y EHCache

  1. Fran dice:

    He configurado la cache de nivel 2 tal y como indicas. Pero aunque no me da error al iniciar la aplicación y que en los logs me aparece HHH000248: Starting query cache at region: Queries. al utilizar alguna query configurada para cache. Si hago un cambio en la BBDD, se me ve en la aplicación. Señal de que la cache no funciona. Que podría estar pasando?

    • danielme.com dice:

      La respuesta la encontrarás depurando línea a línea y comprobando el log en modo debug ya que afortunadamente es bastante completo y explícito (no lo hagas en producción porque ralentiza mucho la ejecución). Si la consulta es cacheable, cuando la solicites verás lo siguiente;
      “Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache”. Puede pasar dos cosas:

      • Si la consulta no está en la caché, se mostrará el mensaje “Query results were not found in cache” y la consulta se lanzará contra la base de datos y podrás verla si activas el showSql de Hibernate. Asimismo, tras obtener los datos verás como estos son cacheados: Caching query results in region: org.hibernate.cache.internal.StandardQueryCache; timestamp=5622198182899712.
      • Si la consulta está en la caché la obtendrás de ahí y lo sabrás gracias al mensaje “Returning cached query results”

      La otra cosa a tener en cuenta es la configuración en la caché del tiempo de permanencia de los objetos en la misma, es posible que se estén cacheando pero cuando se vuelvan a solicitar esos objetos ya no estén porque han expirado.

  2. Lorenzo dice:

    Excelente, muy ilustrativo.

  3. Ale dice:

    Hola, lo he configurado todo como indicas pero mi EntityManager es null parece que no hace ni caso de las anotaciones, ni que tiene que cargar el applicationContext.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: