Spring, JPA y DBUnit

Como ahora tengo bastante tiempo libre, he empezado una aplicación para prototipar algunos conceptos que me interesa fijar. Así, he comenzado por resolver la arquitectura básica de persistencia. He decidido usar Spring y JPA (Hibernate). Y para las pruebas he querido usar también DbUnit para llenar una base de datos en memoria (H2).

Y como estaba bastante oxidado, estuve viendo las nuevas anotaciones en Spring 2 y me llamó la atención el estereotipo . Dado que encaja muy bien con el enfoque de “modelo rico” en el que quiero trabajar, he estado jugando con esto y ha quedado así.

Comenzaré por el test.


@ContextConfiguration(locations = {"classpath:/META-INF/dao-test-context.xml"})
public class UserRepositoryTest extends AbstractRepositoryTest {

@Autowired
private UserDAO userDAO;

@Test
public void testFindByUsername() {
User aUser = userDAO.findByUsername("rob");
assertEquals("0001", aUser.getId());
assertEquals("Robert", aUser.getName());
assertEquals("Smith", aUser.getSurname());
}

La anotación permite decir dónde están los ficheros de configuración de Spring (para nuestro ejemplo sólo usamos uno, pero se pueden poner todos los que se quieran). Con inyectamos el DAO que vamos a probar sin necesidad de declararlo en el xml.

El código de la clase abstracta de la que extendemos tiene la “fixture” de la base de datos usando DbUnit.


@RunWith(SpringJUnit4ClassRunner.class)
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class,TransactionalTestExecutionListener.class})
@Transactional
public class AbstractRepositoryTest {

private static String TEST_DATA_FILE = "src/test/resources/dbunit-test-data.xml";

/** Logger available to subclasses */
protected final Log logger = LogFactory.getLog(getClass());

@Autowired
private DataSource dataSource;

private DataSource getDataSource() {
return this.dataSource;
}

@BeforeTransaction
public void onSetUpInTransaction() throws Exception {
logger.info("*** Inserting test data ***");
// Use spring to get the datasource
DataSource ds = getDataSource();
Connection conn = ds.getConnection();
try {
IDatabaseConnection connection = new DatabaseConnection(conn);
DatabaseOperation.CLEAN_INSERT.execute(connection,
new FlatXmlDataSet(new FileInputStream(TEST_DATA_FILE),false));
} finally {
DataSourceUtils.releaseConnection(conn, ds);
logger.info("*** Finished inserting test data ***");
}
}

@AfterTransaction
public void onTearDownInTransaction() throws Exception {
// Delete the data
DataSource ds = getDataSource();
Connection conn = ds.getConnection();
try {
IDatabaseConnection connection = new DatabaseConnection(conn);
DatabaseOperation.DELETE.execute(connection, new FlatXmlDataSet(
new FileInputStream(TEST_DATA_FILE)));
} finally {
DataSourceUtils.releaseConnection(conn, ds);
logger.info("*** Finished removing test data ***");
}
}

El “truco” aquí consiste en la anotación y la inyección de DataSource (que está declarada en el xml de Spring).

El repositorio lo implementamos como un DAO, pero en vez de heredar de JpaDaoSupport (que es quizás lo más común cuando usamos Spring) lo implementamos independiente de Spring (aunque añadamos la anotación @Repository). Cuando usamos , el contenedor de Spring “sabe” cómo inyectar el EntityManager sin que tengamos que decirle nada explícitamente.


@Repository
public class JpaUserDAO implements UserDAO {

private EntityManager entityManager;

@PersistenceContext
public void setEntityManager(EntityManager entityManager) {
this.entityManager = entityManager;
}

public EntityManager getEntityManager() {
return entityManager;
}

public User findByUsername(final String username) {
return (User) getEntityManager().
createQuery("from User where username = :username").
setParameter("username", username).
getSingleResult();
}

Como es JPA, es necesario el fichero META-INF/persistence.xml.


<persistence version="1.0">
xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">

<persistence-unit name="ACME_PU-TEST" transaction-type="RESOURCE_LOCAL">
<class>acme.core.users.User</class>
</persistence-unit>

</persistence>

Obsérvese que apenas hay que darle información. Tenemos que explicitar qué tenemos (podemos hacerlo en un fichero orm.xml, pero por simplicidad lo he dejado dentro).

Y finalmente, el pegamento de todo esto… el xml de Spring.


<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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd"
default-autowire="byName">

<context:component-scan base-package="acme.core"/>
<context:annotation-config/>

<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="databasePlatform" value="org.hibernate.dialect.H2Dialect"/>
<property name="showSql" value="true"/>
<property name="generateDdl" value="true"/>
</bean>
</property>
<property name="persistenceUnitName" value="ACME_PU-TEST"/>
</bean>

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="org.h2.Driver"/>
<property name="url" value="jdbc:h2:mem:test_mem"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"/>

</beans>

Es importante señalar que hay dos partes en este xml (de hecho, los he fusionado para que resultara más compacto, pero realmente los tengo separados): los elementos del espacio de nombres “context” y “beans” respectivamente.

Las primeras son necesarias para poder usar y otras. Las otras son los únicos tres beans que necesitamos declarar en el xml (porque no son nuestros sino implementaciones que vienen con la librería de Spring).

Es importante declarar los tres y usar esos nombres para aprovechar la inyección automática “byName”.

Falta el xml con los datos para que DbUnit cree la tabla y rellene los datos de prueba (nuestra “fixture”).


<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<user id="0001" username="rob" name="Robert" surname="Smith" />
<user id="0002" username="rob2" name="Robert" surname="Jones" />
<user id="0003" username="peter" name="Peter" surname="Mullins" />
</dataset>

Espero que os sea útil. A continuación os explicaré cómo implementar una capa de servicios que use este repositorio de datos, con sus pruebas de aceptación (con Concordion, por supuesto) y todo… 🙂

  • Jose

    Muchas gracias por esta entrada… lo que estaba esperando desde hace tiempo.

    Por si te sirve de algo, yo he tenido que bajar la anotación:

    @RunWith(SpringJUnit4ClassRunner.class)

    del AbstractRepositoryTest al UserRepositoryTest para evitar que se ejecuten test de una clase que no los tiene.

    Un saludo:

    Jose

  • Jose Manuel Beas

    Muchas gracias a ti.

    Claro, yo lo puse ahí porque quería demostrar su funcionamiento y porque la intención era que todos los que heredaran se ejecutaran, pero muchas gracias por el comentario porque así queda mucho más claro (viendo los pros y los contras de la decisión).

    Un saludo,
    JMB

  • Orlando

    Gracias por el post, una pregunta:

    Funcionara lo expuesto en el post si la db tabla correspondiente a la entidad User es creada en un esquema (schema) especifico?

    Por ejemplo: common.user donde common es el esquema y user es la tabla.

    Saludos,

  • Jose Manuel Beas

    Antes que nada, lo que te voy a comentar no lo he probado.

    Entiendo que tu pregunta tiene que ver con el mapeo de la clase con la tabla, e.d. es específica de JPA. Bien, para lo que pides podemos usar la anotación @Table(name=”User”,schema=”miesquema”) en la clase User. Problema de esta solución: que estás ligando la implementación al despliegue. ¿Qué pasaría si tuvieras que cambiar el esquema de la bd? Que tendrías que cambiar el código fuente, recompilar y volver a desplegar el módulo. 🙁

    Insisto, no lo he probado, “pero debería funcionar”. 🙂

    Un saludo,
    JMB

  • Jorge Arturo

    buenas me parece muy bueno tu post, y quería ver si me podias ayudar ya que estoy intentado usar hibernate y persistence, lo que ocupo hacer es cambiar las propiedades de conexion que están declaradas en el xml enviandole nuevos valores desde un frame… por ejemplo que en mi xml tenia como server. localhost pero en frame lo quiero cambiar a la ip 192.168.1.5 si me pudieras ayudar te lo agradecería montones ya tengo bastante de buscar y no lo he podido realizar

    de antemano muchas gracias

  • Jose Manuel Beas

    Hola Jorge Arturo,

    La verdad es que no termino de entender lo que me quieres decir con lo del "frame", me descoloca un poco.

    Si te refieres a la IP de tu servidor de BD, yo en mi ejemplo he usado H2 con su configuración "en memoria", por eso no ves ninguna IP que puedas cambiar. Suponiendo que usas MySQL o algo parecido, debes cambiar la configuración del bean "dataSource". Normalmente tu IP irá en el parámetro URL.

    Espero que sea eso lo que necesitas.

    Un saludo,
    JMB

  • Hola José Manuel. Ante todo muchas gracias por este artículo tan interesante. Te escribo porque a pesar de todo sigo con una duda transcendental. Verás.

    En el DAO vemos que tenemos un EntityManager cuyo valor nos inyecta Spring. De acuerdo, pero si no me equivoco el DAO es un Singleton ya que por defecto el Scope de los beans de Spring son Singleton. Dicho esto, entonces se deduce que el EntityManager está compartido por todos los servicios que en un instante cualquiera utilicen el mismo DAO.

    Teniendo en cuenta que el ORM subyacente (hibernate) puede usar sus cachés a nivel de EntityManager pienso que puede ser desastroso compartir el mismo EntityManager entre diferentes hilos concurrentes que están dedicados a peticiones de diversos usuarios.

    Por ahí se ven soluciones de gestión del EntityManager a nivel de petición Httpd, pero no acabo de entender si Spring nos soluciona el problema web o no.

    No sé si habré conseguido explicarme 🙂

  • jmbeas

    Hola Luis,

    Pues la verdad es que no recuerdo si me preocupé por este problema o no en su momento (no tengo ningún test que lo demuestre desde luego) y tampoco tengo mucho tiempo ahora para comprobarlo, pero he googleado un poco y tengo la sospecha de que no es un problema.

    Por favor, echa un vistazo a esto y dime si resuelve tu duda. Lógicamente, lo suyo sería hacer un test para demostrarlo, pero ahora mismo no tengo muy claro cómo demostrar esta posible condición de carrera y teniendo en cuenta que pudiera no darse…