En esta tercera parte de mi serie sobre Spring, vamos a continuar con el patrón DAO.
En el capítulo anterior, sentábamos la base sobre la que se contruirá toda nuestra capa DAO. En este capítulo, crearemos una base de datos muy simple, con una sola tabla, y crearemos su entidad de Hibernate correspondiente y su DAO.
También aprovecharé para hablar sobre los tests. Sin ellos, para ver el código en acción, aún nos faltaría como mínimo un interfaz visual. Pero nos interesa probar el código sin tener que crear este interfaz. Así que crearemos una base de datos para testing en memoria, usando HSQLDB, con datos iniciales, para poder ejecutar operaciones de consulta y comprobar que funcionan.
Creación del modelo de datos
Como aficionado al juego World Of Warcraft ®, he pensado crear una base de datos para gestionar los jugadores de una hermandad. En este episodio, por simplificar, crearé una sola entidad, Player, y en próximos artículos iré ampliándo el modelo de datos con nuevas entidades y relaciones.
Vamos a crear un nuevo proyecto java que contendrá todas las entidades anotadas mediante JPA (Java Persistence API) por motivos de modularidad y portabilidad. Nos permitirá usar nuestra capa de datos en otros entornos, como en EJB, y con otros motores de persistencia que implementen JPA.
Lo primero es crear el proyecto:
mvn archetype:create -DgroupId=com.genbetadev.spring -DartifactId=spring-sample-domain
La entidad es muy sencilla, sólo tendrá 3 campos: id del jugador, nombre y apellidos:
package com.genbetadev.spring.domain; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; @Entity(name = “Player”) @Table(name = “Player”) public class Player implements java.io.Serializable { /** * serialVersionUID */ private static final long serialVersionUID = 6485105191418783803L; @Id @Column(name = “playerId”, length = 50) private String playerId; @Column(name = “playerFirstName”, length = 50, nullable = false) private String playerFirstName; @Column(name = “playerLastName”, length = 50, nullable = false) private String playerLastName; public String getPlayerId() { return playerId; } public void setPlayerId(String playerId) { this.playerId = playerId; } public String getPlayerFirstName() { return playerFirstName; } public void setPlayerFirstName(String playerFirstName) { this.playerFirstName = playerFirstName; } public String getPlayerLastName() { return playerLastName; } public void setPlayerLastName(String playerLastName) { this.playerLastName = playerLastName; } @Override public String toString() { return “Player [playerId=” + playerId + “, playerFirstName=” + playerFirstName + “, playerLastName=” + playerLastName + “]”; } }
No es pertinente para este artículo esplicar el API de persistencia de java, aunque aún sino lo conocéis, es bastante fácil de comprender, veamos las anotaciones que he utilizado:
@Entity: indica que la clase en una entidad
@Table: indica que tabla estamos mapeando en la clase
@Id: indica la propiedad que actúa como identificador
@Column: indica contra que columna de la tabla se mapea una propiedad
Esto será nuestra capa de datos, muy simple de momento. Ahora toca crear su DAO correspondiente para realizar operaciones sobre nuestra entidad.
Implementación del DAO
En el capítulo anterior, ya sentamos la base para nuestros DAOs, y ahora lo utilizaremos para crear nuestro PlayerDao:
package com.genbetadev.spring.dao; import com.genbetadev.spring.domain.Player; public interface PlayerDao extends BaseDao<Player , String> { }
Queremos que nuestro DAO tenga todas las operaciones que especificamos en BaseDao, así que lo extendemos. Mediante generics, especificamos que nuestro DAO es sobre la entidad Player y su id es de tipo String. En este caso, nuestro DAO no tiene ninguna operación adicional, así que no especificamos ningún método, sino que sólo tendrá los que por defecto especificamos en BaseDao.
Ahora, creamos la implementación de PlayerDao:
package com.genbetadev.spring.dao; import java.util.List; import com.genbetadev.spring.domain.Player; public class PlayerDaoImpl extends BaseDaoHibernate<Player , String> implements PlayerDao { @Override public ListfindAll() throws Exception { return getHibernateTemplate().loadAll(Player.class); } @Override public Player findById(String id) throws Exception { return getHibernateTemplate().get(Player.class, id); } }
¿Porqué implementamos sólo estos dos métodos? porqué los demás ya tienen una implementación por defecto en BaseDaoHibernate, excepto estos dos que eran abstract y por tanto se deben implementar en las clases hija.
Así de sencillo, ya tenemos implementado nuestro DAO para atacar a la entidad Player. Ahora, veámoslo en acción.
Testeando nuestro DAO
Para probar nuestro DAO, hay que tener base de datos. Pare ello usaremos una BD en memória como es HSQLDB. Lo primero es crear el esquema. He metido un script de creación de la BD en src/test/resources/schema.sql:
drop table Player if exists; create table Player(playerId varchar(50) primary key, playerFirstName varchar(50) not null, playerLastName varchar(50) not null );
Y unos datos de prueba en src/test/resources/test-data.sql:
insert into Player(playerId, playerFirstName, playerLastName) values (‘player01@gmail.com’,‘name01’,‘lastname01’); insert into Player(playerId, playerFirstName, playerLastName) values (‘player02@gmail.com’,‘name02’,‘lastname02’); insert into Player(playerId, playerFirstName, playerLastName) values (‘player03@gmail.com’,‘name03’,‘lastname03’); insert into Player(playerId, playerFirstName, playerLastName) values (‘player04@gmail.com’,‘name04’,‘lastname04’); insert into Player(playerId, playerFirstName, playerLastName) values (‘player05@gmail.com’,‘name05’,‘lastname05’); insert into Player(playerId, playerFirstName, playerLastName) values (‘player06@gmail.com’,‘name06’,‘lastname06’); insert into Player(playerId, playerFirstName, playerLastName) values (‘player07@gmail.com’,‘name07’,‘lastname07’);
Ahora, crearemos el contexto de spring para nuestros tests en src/test/resources/spring-test-dao-context.xml:
<?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:aop=“http://www.springframework.org/schema/aop” xmlns:jdbc=“http://www.springframework.org/schema/jdbc” xsi:schemaLocation=” http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd”> <!— Creates a data source that can provide a connection to in-memory embedded database populated with test data see: http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/jdbc.html#jdbc-embedded-database-support —> <jdbc:embedded-database id=“dataSource”> <jdbc:script location=“classpath:schema.sql” /> <jdbc:script location=“classpath:test-data.sql” /> </jdbc:embedded-database> <bean id=“sessionFactory” class=“org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean”> <property name=“dataSource” ref=“dataSource” /> <property name=“packagesToScan” value=“com.genbetadev.spring.domain” /> </bean> <bean id=“hibernateTemplate” class=“org.springframework.orm.hibernate3.HibernateTemplate”> <property name=“sessionFactory” ref=“sessionFactory”/> </bean> <bean id=“playerDao” class=“com.genbetadev.spring.dao.PlayerDaoImpl”> <property name=“hibernateTemplate” ref=“hibernateTemplate”/> </bean> </beans>
Esto ya es Spring puro y duro. Ya comenté que Spring actúa de contenedor de objetos. Esto es un application context de Spring donde se definen los componentes de nuestra aplicación y su relación entre ellos. Cada componente tiene un identificador (id) y la clase que estamos instanciando (class). Vamos a ver que es cada uno de ellos por su identificador:
dataSource: La fuente de datos, en este caso una BD empotrada. Sino se especifica nada, por defecto Spring usa HSQLDB, cuyo driver jdbc debe estar en el classpath (ya está incluido en nuestro pom.xml). Lo único que le decimos es que cargue nuestros scripts sql.
sessionFactory: El SessionFactory de hibernate. Utilizo la clase AnnotationSessionFactoryBean, ya que se utiliza para cargar entidades anotadas mediante Java Persistence API. Le inyectamos nuestro datasource y especificamos el paquete donde se encuentran nuestras entidades. El atributo “ref” se utiliza para referenciar otros objectos del application context, en este caso, nuestro datasource. Es la manera de crear relaciones entre nuestros componentes. Para ello Spring utiliza los setters de las clases, luego si queremos saber que podemos inyectar en nuestro sessionFactory, tenemos que abrir la documentación de la clase AnnotationSessionFactoryBean y ver que “setters” tiene. Si en vez de entidades anotadas mediante JPA usaramos mapas de hibernate (*.hbm.xml), usaríamos LocalSessionFactoryBean. Pero trato de evitar tener muchos ficheros XML, además que usando JPA me permite cambiar a otro motor de persistencia relativamente fácil.
hibernateTemplate: Si recordáis del capítulo anterior, nuestro BaseDaoHibernate implementaba HibernateDaoSupport que es la clase que nos proporciona HibernateTemplate, una plantilla para realizar operaciones con Hibernate. Lo que estamos haciendo aquí es crear una instancia para poder inyectársela a nuestros DAO.
playerDao: Finalmente creamos una instancia de nuestro playerDao y le inyectamos la instancia de hibernateTemplate. Sino lo hicieramos, todos las operaciones de nuestro Dao arrojarían un NullPointerException ya que hibernateTemplate sería null.
Bueno, pues ya lo tenemos todo prácticamente. Sólo nos falta un detalle, cargar el application context. Para ello, vamos a crearnos una clase de test, que cargará nuestro contexto de Spring y ejecutará un par de tests sobre nuestro PlayerDao. Vamos a crear la clase PlayerDaoTest en src/test/java/:
package com.genbetadev.spring.dao; 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.util.Assert; import com.genbetadev.spring.domain.Player; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(”/spring-test-dao-context.xml”) public class PlayerDaoTest { @Autowired private PlayerDao playerDao; private static final String PLAYERID = “player01@gmail.com”; /** * @param playerDao * the playerDao to set */ public void setPlayerDao(PlayerDao playerDao) { this.playerDao = playerDao; } @Before public void setUp() throws Exception { } @Test public void testFindAll() throws Exception { Listresult = playerDao.findAll(); Assert.notNull(result); System.out.println(result.toString()); } @Test public void testFindByIdString() throws Exception { Player result = playerDao.findById(PLAYERID); Assert.notNull(result); System.out.println(result.toString()); } }
No voy a profundizar ahora en el testing, básicamente que sepáis que cada test se anota mediante @Test en el método. Estos test se ejecutarán cuando compilemos con maven durante la fase de test. Si fallan, la compilación fallará.
Lo primero es cargar nuestro contexto de spring:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(”/spring-test-dao-context.xml”)
@RunWith es para decirle a JUnit que utilize el “Runner” de Spring. Y @ContextConfiguration le dice a Spring donde está definido nuestro contexto. Ahora , cuando ejecutemos los tests, el contexto se cargará.
Fijaos que nuestra clase de test tiene una propiedad de tipo PlayerDao, que es la clase que queremos probar. Tenemos que decirle a Spring que nos inyecte la instancia que tenemos en nuestro contexto. La manera más fácil es usando la anotación @Autowired:
@Autowired private PlayerDao playerDao; /** * @param playerDao * the playerDao to set */ public void setPlayerDao(PlayerDao playerDao) { this.playerDao = playerDao; }
¿Que és @Autowired? pues es una anotación de Spring para darle el poder de buscar que componente inyectar. Hay que llevar cuidado con esta anotación. En nuestro caso, sólo hay un candidato que se puede inyectar, y es nuestra instancia de playerDao declarada en el application context, pues es el único que implementa la interfaz PlayerDao. Pero, si tuvieramos mas de una implementación, puede que Spring inyectara la que no queremos. En todo caso, si queremos estar 100% seguros, siempre podemos sustituir @Autowired por @Resource(name=“PlayerDao”) para indicar exactamente que componente queremos inyectar. Resource es una anotación estándar de J2EE (JSR-250), cuya dependencia habría que añadir al pom. En cualquiera caso, no hay que olvidar definir el setter de playerDao, o Spring no tendrá manera de “setear” nuestra dependencia.
Ya está todo listo para probar nuestro DAO. Vamos a ejecutar un “mvn install” y a ver lo que pasa:
———————————————————————————- T E S T S ———————————————————————————- Running com.genbetadev.spring.dao.PlayerDaoTest 07-nov-2011 23:36:05 org.springframework.test.context.TestContextManager retrieveTestExecutionListeners INFO: @TestExecutionListeners is not present for class [class com.genbetadev.spring.dao.PlayerDaoTest]: using defaults. 07-nov-2011 23:36:05 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions INFO: Loading XML bean definitions from class path resource [spring-test-dao-context.xml] 07-nov-2011 23:36:05 org.springframework.context.support.AbstractApplicationContext prepareRefresh INFO: Refreshing org.springframework.context.support.GenericApplicationContext@58d9660d: startup date [Mon Nov 07 23:36:05 GMT 2011]; root of context hierarchy 07-nov-2011 23:36:05 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@235dd910: defining beans [dataSource,sessionFactory,hibernateTemplate,playerDao,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.internalPersistenceAnnotationProcessor]; root of factory hierarchy 07-nov-2011 23:36:05 org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactory initDatabase INFO: Creating embedded database ‘dataSource’ 07-nov-2011 23:36:05 org.springframework.jdbc.datasource.init.ResourceDatabasePopulator executeSqlScript INFO: Executing SQL script from class path resource [schema.sql] 07-nov-2011 23:36:05 org.springframework.jdbc.datasource.init.ResourceDatabasePopulator executeSqlScript INFO: Done executing SQL script from class path resource [schema.sql] in 4 ms. 07-nov-2011 23:36:05 org.springframework.jdbc.datasource.init.ResourceDatabasePopulator executeSqlScript INFO: Executing SQL script from class path resource [test-data.sql] 07-nov-2011 23:36:05 org.springframework.jdbc.datasource.init.ResourceDatabasePopulator executeSqlScript INFO: Done executing SQL script from class path resource [test-data.sql] in 7 ms. SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder”. SLF4J: Defaulting to no-operation (NOP) logger implementation SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details. 07-nov-2011 23:36:06 org.springframework.orm.hibernate3.LocalSessionFactoryBean buildSessionFactory INFO: Building new Hibernate SessionFactory [Player [playerId=player01@gmail.com, playerFirstName=name01, playerLastName=lastname01], Player [playerId=player02@gmail.com, playerFirstName=name02, playerLastName=lastname02], Player [playerId=player03@gmail.com, playerFirstName=name03, playerLastName=lastname03], Player [playerId=player04@gmail.com, playerFirstName=name04, playerLastName=lastname04], Player [playerId=player05@gmail.com, playerFirstName=name05, playerLastName=lastname05], Player [playerId=player06@gmail.com, playerFirstName=name06, playerLastName=lastname06], Player [playerId=player07@gmail.com, playerFirstName=name07, playerLastName=lastname07]] Player [playerId=player01@gmail.com, playerFirstName=name01, playerLastName=lastname01] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.075 sec Results : Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 07-nov-2011 23:36:06 org.springframework.context.support.AbstractApplicationContext doClose INFO: Closing org.springframework.context.support.GenericApplicationContext@58d9660d: startup date [Mon Nov 07 23:36:05 GMT 2011]; root of context hierarchy [INFO] [INFO] —- maven-jar-plugin:2.3.1:jar (default-jar) @ spring-sample-dao —- [INFO] Building jar: /Users/mangrar/workspaces/Eclipse Indigo/spring-sample-dao/target/spring-sample-dao-0.0.1-SNAPSHOT.jar [INFO] [INFO] —- maven-install-plugin:2.3.1:install (default-install) @ spring-sample-dao —- [INFO] Installing /Users/mangrar/workspaces/Eclipse Indigo/spring-sample-dao/target/spring-sample-dao-0.0.1-SNAPSHOT.jar to /Users/mangrar/.m2/repository/com/genbetadev/spring/spring-sample-dao/0.0.1-SNAPSHOT/spring-sample-dao-0.0.1-SNAPSHOT.jar [INFO] Installing /Users/mangrar/workspaces/Eclipse Indigo/spring-sample-dao/pom.xml to /Users/mangrar/.m2/repository/com/genbetadev/spring/spring-sample-dao/0.0.1-SNAPSHOT/spring-sample-dao-0.0.1-SNAPSHOT.pom [INFO] ———————————————————————————————————— [INFO] BUILD SUCCESS [INFO] ———————————————————————————————————— [INFO] Total time: 2.721s [INFO] Finished at: Mon Nov 07 23:36:06 GMT 2011 [INFO] Final Memory: 5M/81M [INFO] ————————————————————————————————————
Si observáis la salida de la consola, podéis ver como Spring va cargando los beans y nos informa de la ejecución de los scripts sql. Podemos ver también la salida de nuestros tests, que nos muestra el resultado devuelto por nuestro DAO. Por último, Maven nos informa de que ha ejecutado 2 tests con éxito:
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.075 sec
Aunque no es el objetivo de este artículo profundizar en el testing, voy a comentarlo un poco. Mi intención simplemente con estos tests es comprobar que ambos métodos me devuelven resultados:
Assert.notNull(result);
Esta instrucción le dice a JUnit que compruebe que result no es nulo. Si lo és, el test falla. En estos momentos no estoy haciendo un test de integridad, es decir, no compruebo que resultados me devuelve, sino simplemente que me devuelva algo.
La próxima semana ampliaré el modelo de datos y empezaré a crear la capa de negocio de nuestra aplicación Spring.
Mas información: Introducción a Spring Framewok ¦ El patrón DAO ¦ Spring reference manual
Código fuente: spring-sample-domain ¦ spring-sample-dao