Spring, JPA y DBUnit

Palabras clave:
Tiempo aproximado: 3 min.
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 @Repository. 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 @ContextConfiguration 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 @Autowired 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 @Transactional 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 @Repository, 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é @Entity 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 @Autowire 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… 🙂