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.
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:
- 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.
- 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" />
-
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).
- 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}" />
- 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>
- 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:
- 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>
-
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.
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
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?
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:
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.
Excelente, muy ilustrativo.
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.
Una consulta, cuando se usa pool de conexiones no es necesario liberar la conexion con el metodo .close()?
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.