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

Última actualización: 01/09/2017

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 terceros frameworks y estándares JEE. El núcleo de Spring (Spring IoC Container) puede hacer de «pegamento» en nuestros proyectos entre las distintas tecnologías que utilicemos, 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 visto estas posibilidades de integración con los tutoriales sobre servicios web SOAP en los que utilizamos el core de Spring en una aplicación web que implementa esos servicios SOAP utilizando el estándar JAX-WS con Apache CXF. En esta ocasión, veremos 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. En concreto, se va a utilizar Hibernate, siguiendo en la medida de lo posible la especificación JPA, y que es 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.

Cursos de programación

Proyecto para pruebas

Para las pruebas se usará un proyecto Maven no web cuya base es 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>
	<packaging>jar</packaging>

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

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<java.version>1.7</java.version>
		<maven.version>3.0</maven.version>

		<spring.version>4.3.10.RELEASE</spring.version>
		<hibernate.version>5.1.9.Final</hibernate.version> <!-- 5.2 requires Java 8 -->
		<c3p0.version>0.9.1.2</c3p0.version>
		<log4j.version>1.2.17</log4j.version>
		<slf4j.version>1.7.25</slf4j.version>
		<mysql.driver.version>5.1.43</mysql.driver.version>
		<junit.version>4.12</junit.version>

		<maven.compiler.plugin.version>3.6.1</maven.compiler.plugin.version>
		<maven.jar.plugin.version>3.0.2</maven.jar.plugin.version>
		<enforcer.plugin>1.4.1</enforcer.plugin>
	</properties>

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

				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-enforcer-plugin</artifactId>
					<version>${enforcer.plugin}</version>
					<executions>
						<execution>
							<id>enforce-java</id>
							<goals>
								<goal>enforce</goal>
							</goals>
							<configuration>
								<rules>
									<requireJavaVersion>
										<version>${java.version}</version>
									</requireJavaVersion>
									<requireMavenVersion>
										<version>${maven.version}</version>
									</requireMavenVersion>
								</rules>
							</configuration>
						</execution>
					</executions>
				</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>

		<!-- log4j -> sl4j bridge for Spring -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-log4j12</artifactId>
			<version>${slf4j.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. Este módulo es una dependencia de spring-orm por lo que no es necesario declararlo en el pom.
  • 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.
  • log4j: lo usaremos para que ver qué pasa…
  • JUnit y spring-test: el código se probará con tests de JUnit integrados con Spring siguiendo lo explicado en el tutorial Testing Spring con JUnit 4.

Aunque tal y como podemos ver el proyecto se basa en Spring 4 e Hibernate 5, en la primera versión de este tutorial se utilizaba Spring 3.2 e Hibernate 4.3 y la integración que veremos también puede aplicarse a esas versiones más antiguas.

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>    

	<!-- prints the SQL (Prepared Statement) generated by Hibernate -->
   	<logger name="org.hibernate.SQL">
		<level value="debug" />
	</logger>
	
	<!-- prints the querys parameters -->
	<logger name="org.hibernate.type">
		<level value="trace" />
	</logger>  
 
    <root>
        <priority value="debug" />
        <appender-ref ref="console" />
    </root>
 
</log4j:configuration>

Configuración de Spring

Vamos a proceder a encajar y configurar todas las piezas del puzzle en el fichero /src/main/resources/applicationContext.xml (en la sección siguiente veremos cómo hacer lo mismo pero con JavaConfig en lugar de XML). Vayamos 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 configuración 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 tutorial 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, aunque recientemente estoy utilizando en su lugar HikariCP. 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. 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 comentado, 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
    
    #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
    hibernate.format_sql = true
    #use log4j instead
    hibernate.show_sql = false 
    
    #c3p0 pool
    c3p0.acquire_increment=5
    c3p0.max_size=100
    c3p0.min_size=5
    c3p0.max_idle_time=1200
    c3p0.unreturned_connection_timeout=120
    

    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. Asimismo, se incluye la configuración de la caché de segundo nivel que veremos más adelante.
    	
    	<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>
                    <prop key="hibernate.format_sql">${hibernate.format_sql}</prop>
                    <prop key="hibernate.show_sql">${hibernate.show_sql}</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>
                <prop key="hibernate.format_sql">${hibernate.format_sql}</prop>
                <prop key="hibernate.show_sql">${hibernate.show_sql}</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 no es necesario porque 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">

Configuración con JavaConfig

Cada vez es más habitual que las configuraciones de Spring se hagan de forma programática en lugar de los tradicionales ficheros XML. De hecho, hace ya bastante tiempo que la mayoría de tutoriales e incluso los ejemplos oficiales de Spring sólo utilizan esta opción.

A continuación muestro una clase que realiza exactamente la misma configuración que hemos visto en la sección anterior.

package com.danielme.demo.springjpahib;

import java.beans.PropertyVetoException;
import java.util.Properties;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.mchange.v2.c3p0.ComboPooledDataSource;

@Configuration
@PropertySource("classpath:db.properties")
@ComponentScan("com.danielme")
@EnableTransactionManagement
public class ApplicationContext {

    @Bean(destroyMethod = "close")
    DataSource dataSource(Environment env) {
        ComboPooledDataSource ds = new ComboPooledDataSource();
        try {
            ds.setDriverClass(env.getRequiredProperty("jdbc.driverClassName"));
        } catch (IllegalStateException | PropertyVetoException ex) {
            throw new RuntimeException(
                    "error while setting the driver class name in the datasource", ex);
        }
        ds.setJdbcUrl(env.getRequiredProperty("jdbc.url"));
        ds.setUser(env.getRequiredProperty("jdbc.username"));
        ds.setPassword(env.getRequiredProperty("jdbc.password"));
        ds.setAcquireIncrement(env.getRequiredProperty("c3p0.acquire_increment", Integer.class));
        ds.setMinPoolSize(env.getRequiredProperty("c3p0.min_size", Integer.class));
        ds.setMaxPoolSize(env.getRequiredProperty("c3p0.max_size", Integer.class));
        ds.setMaxIdleTime(env.getRequiredProperty("c3p0.max_idle_time", Integer.class));
        ds.setUnreturnedConnectionTimeout(env.getRequiredProperty(
                "c3p0.unreturned_connection_timeout", Integer.class));

        return ds;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
            Environment env) {
        LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean();
        //entityManager.setPersistenceXmlLocation("classpath*:META-INF/persistence.xml");
        //entityManager.setPersistenceUnitName("hibernatePersistenceUnit");
        entityManager.setPackagesToScan("com.danielme.demo.springjpahib");
        entityManager.setDataSource(dataSource);
        entityManager.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        
        Properties jpaProperties = new Properties();      
        jpaProperties.put("hibernate.hbm2ddl.auto", env.getRequiredProperty("hibernate.hbm2ddl.auto"));
        jpaProperties.put("hibernate.dialect", env.getRequiredProperty("hibernate.dialect"));
        jpaProperties.put("hibernate.connection.autocommit", env.getRequiredProperty("hibernate.connection.autocommit"));
        jpaProperties.put("hibernate.cache.use_second_level_cache", env.getRequiredProperty("hibernate.cache"));
        jpaProperties.put("hibernate.cache.use_query_cache", env.getRequiredProperty("hibernate.query.cache"));
        jpaProperties.put("hibernate.cache.provider_class", "org.hibernate.cache.EhCacheProvider");
        jpaProperties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory");
        jpaProperties.put("hibernate.generate_statistics", env.getRequiredProperty("hibernate.statistics"));        
        jpaProperties.put("hibernate.show_sql", env.getRequiredProperty("hibernate.show_sql"));        
        jpaProperties.put("hibernate.format_sql", env.getRequiredProperty("hibernate.format_sql"));        
       
        entityManager.setJpaProperties(jpaProperties);

        return entityManager;
    }
    
    @Bean
    JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }
}

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.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import org.hibernate.annotations.GenericGenerator;

/**
 * Sample JPA entity.
 * @author danielme.com
 *
 */
@org.hibernate.annotations.Cache(usage = org.hibernate.annotations.CacheConcurrencyStrategy.READ_WRITE)
@Entity
@Table(name="countries")
public class Country
{
    //Use the IDENTITY generator instead of TABLE in MySQL and Hibernate 5 
    @Id
    @GeneratedValue(
        strategy= GenerationType.AUTO, 
        generator="native"
    )
    @GenericGenerator(
        name = "native", 
        strategy = "native"
    )
	 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 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, siguiendo lo expuesto en el tutorial Testing Spring con JUnit 4. 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.

Si usamos Spring 4.1, una alternativa para iniciar los datos de pruebas de la BD es utilizar la anotacón @Sql para ejecutar un script.

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(classes = { ApplicationContext.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.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>
    
    

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

Master Pyhton, Java, Scala or Ruby

EHCache es muy flexible, 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");
		countryDao.getById(country.getId());
		// 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.

Otros tutoriales relacionados con Spring y/o Hibernate

Introducción a Spring Boot: Aplicación Web con servicios REST y Spring Data JPA

Spring REST: Securización BASIC y JDBC con Spring Security

JPA e Hibernate: relaciones y atributos Lazy

Spring JDBC Template: simplificando el uso de SQL

Persistencia en BD con Spring Data JPA

Testing Spring con JUnit 4

JPA + Hibernate: Claves primarias

Ficheros .properties en Spring IoC

6 comentarios sobre “Persistencia en BD con Spring: Integrando JPA, c3p0, Hibernate y EHCache

  1. 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?

    1. 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. 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.

    1. Efectivamente, hay que devolver la conexión al pool cuando se haya terminado de utilizarla pero en el ejemplo la gestión de transacciones de Spring ya se encarga de forma automática de obtener y liberar la conexión.

Deja un comentario

Este sitio utiliza Akismet para reducir el spam. Conoce cómo se procesan los datos de tus comentarios.