RSS

Don’t repeat the DAO – No repitas el DAO con Spring e Hibernate

23 Jun

Para este post he seguido el artículo Don’t repeat the DAO de Per Mellqvist.

El patrón DAO indica que por cada clase de dominio (entidad) que tengamos necesitamos crear un DAO (Data Access Object) que se encargue de persistir o buscar en base de datos la información. ¿Cuál es el inconveniente?. Pues que para todas las entidades van a existir una serie de operaciones básicas (CRUDs) y operaciones de búsqueda que van a ser comunes para cualquier entidad. En Hibernate podemos solucionar esto en parte utilizando las operaciones de sesión CRUD. El problema de ésto es que perdemos la seguridad de tipos en las operaciones por lo que tendremos que estar continuamente haciendo casting a dichas operaciones.

En este punto aparecen las coleciones de tipo seguro disponibles a partir de Java5 y Spring AOP para evitarlo. ¿Qué es lo que vamos a hacer?.

Crearemos una interfaz DAO genérica con los principales métodos usados para manipular nuestras entidades en la base de datos. Igual que en el ejemplo del artículo vamos a crear operaciones para crear, leer, modificar y eliminar los objetos de nuestro modelo de datos. Si nos fijamos en la definición de la interfaz notamos que está parametrizada por una clase genérica y una clave primaria que a su vez sea serializable. Esto permitirá que independientemente del tipo de nuestra clase podamos acceder a las operaciones de modo tipo seguro.

package com.hopcroft.examples.dao;

import java.io.Serializable;

public interface GenericDao  {

    /** Persist the newInstance object into database */
    PK create(T newInstance);

    /** Retrieve an object that was previously persisted to the database using
     *   the indicated id as primary key
     */
    T read(PK id);

    /** Save changes made to a persistent object.  */
    void update(T transientObject);

    /** Remove an object from persistent storage in the database */
    void delete(T persistentObject);
}

La implementación de esta interfaz será bastante obvia. Si hay algo que llame la atención es que nuestra implementación implementa la interfaz FinderExecutor. FinderExecutor es una interfaz que: «permite mapear metodos definidos en el DAO a named queries». Con ello no sólo vamos a poder evitar la repetición de código para los CRUD’s sino también para named queries comunes.

package com.hopcroft.examples.dao;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.List;

import org.hibernate.Query;
import org.openxma.dsl.platform.dao.FinderExecutor;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

public class GenericDaoHibernateImpl
extends HibernateDaoSupport implements GenericDao, FinderExecutor {
    private Class type;

    public GenericDaoHibernateImpl(Class type) {
        this.type = type;
    }

    public PK create(T o) {
//        return (PK) getSession().save(o);
    	return (PK) getHibernateTemplate().save(o);
    }

    public T read(PK id) {
        return (T) getSession().get(type, id);
    }

    public void update(T o) {
        getSession().update(o);
    }

    public void delete(T o) {
        getSession().delete(o);
    }

    public List executeFinder(Method method, final Object[] queryArgs) {
         final String queryName = queryNameFromMethod(method);
         final Query namedQuery = getSession().getNamedQuery(queryName);
         String[] namedParameters = namedQuery.getNamedParameters();
         for(int i = 0; i < queryArgs.length; i++) {
                 Object arg = queryArgs[i];
                 namedQuery.setParameter(i, arg);
          }
          return (List) namedQuery.list();
     }

     public String queryNameFromMethod(Method finderMethod) {
         return type.getSimpleName() + "." + finderMethod.getName();
     }

	public Iterator iterateFinder(Method arg0, Object[] arg1) {
		// TODO Auto-generated method stub
		return null;
	}

            }

Me he creado una tabla (Car) en mySql donde persistir los datos. La clase java que mapea dicha entidad se muestra a continuación. De momento vamos a dejar sin explicar las Named Queries.

package com.hopcroft.examples.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

import org.hibernate.annotations.NamedQueries;
import org.hibernate.annotations.NamedQuery;

@Entity
@Table(name = "Car")
@NamedQueries({
@NamedQuery(name = "Car.findCars", query = "select c from Car c") ,
@NamedQuery(name = "Car.findCarsByCompany", query = "select c from Car c where c.company = ? ")
})
public class Car {
	@Id
	@Column(name = "id")
	private long id;
	@Column(name = "company")
	private String company;
	@Column(name = "model")
	private String model;
	@Column(name = "price")
	private long price;

	public void setId(long id) {
		this.id = id;
	}

	public long getId() {
		return id;
	}

	public String getCompany() {
		return company;
	}

	public void setCompany(String company) {
		this.company = company;
	}

	public String getModel() {
		return model;
	}

	public void setModel(String model) {
		this.model = model;
	}

	public long getPrice() {
		return price;
	}

	public void setPrice(long price) {
		this.price = price;
	}

}

Para configurar nuestro proyecto modificamos el application context de Spring añadiendo un dataSource que apunte a nuestra base de datos. Añadimos un sessionFactory en el cual inyectaremos el dataSource previamente creado. Finalmente crearemos un bean con el dao para la entidad que tenemos en la base de datos.
Con esto ya tendremos todo lo necesario para realizar CRUDs con tipado seguro sobre todas nuestras entidades pero también queremos tener consultas, es hay donde llegan las Named Queries.

Para realizar Named Queries necesitamos introducir Spring AOP en nuestro proyecto. Lo que vamos a obtener un proxy a partir de nuestro DAO generico. Este proxy a su vez va a tener un handler que es el que se va a encargar de las named queries. En el fichero de configuración vamos a tener tres beans.

FinderIntroductionAdvisor. Creará un nuevo objeto FinderIntroductionInterceptor que se encargará a su vez de invocar el método executeFinder del GenericDaoHibernateImpl que se encargará de llamar a la named Query que nos interese, además de establecer los parametros para esa query en cuestión.

package com.hopcroft.examples.dao;

import org.springframework.aop.support.DefaultIntroductionAdvisor;

@SuppressWarnings("serial")
public class FinderIntroductionAdvisor extends DefaultIntroductionAdvisor {
    public FinderIntroductionAdvisor() {
        super(new FinderIntroductionInterceptor());
    }
}

 

package com.hopcroft.examples.dao;

import java.lang.reflect.Method;

import org.aopalliance.intercept.MethodInvocation;
import org.openxma.dsl.platform.dao.FinderExecutor;
import org.openxma.dsl.platform.dao.FinderNamingStrategy;
import org.openxma.dsl.platform.dao.impl.ExtendedFinderNamingStrategy;
import org.openxma.dsl.platform.dao.impl.SimpleFinderNamingStrategy;
import org.springframework.aop.IntroductionInterceptor;

public class FinderIntroductionInterceptor implements IntroductionInterceptor {

	public Object invoke(MethodInvocation invocation) throws Throwable {
		FinderNamingStrategy fns = new SimpleFinderNamingStrategy();
		String queryNameFromMethod = fns.queryNameFromMethod(
				invocation.getClass(), invocation.getMethod());
		FinderExecutor genericDao = (FinderExecutor) invocation.getThis();
		String methodName = invocation.getMethod().getName();
		if (methodName.startsWith("find")) {
			Object[] arguments = invocation.getArguments();
			return genericDao.executeFinder(invocation.getMethod(), arguments);
		} else {
			return invocation.proceed();
		}
	}

	public boolean implementsInterface(Class intf) {
		return intf.isInterface()
				&& FinderExecutor.class.isAssignableFrom(intf);
	}

}

AbstractDaoTarget. bean abstracto que tendrá inyectado la sessionFactory necesaria para persistir.
AbstractDao. nuestro proxy el cual podrá ejecutar a través del FinderIntroductionAdvisor las named queries.

Los siguientes pasos a realizar son:
– crear nuestra interfaz para el DAO específico que queremos (CarDAO) donde definiremos las búsquedas que queremos hacer.

package com.hopcroft.examples.dao;

import java.io.Serializable;
import java.util.List;

import com.hopcroft.examples.domain.Car;

public interface CarDao extends GenericDao{
	public List findCars();
	public List findCarsByCompany(String company);
}

– Implementar estas búsquedas mediante Named queries. En el ejemplo se han realizados en el fichero de mapping para hibernate de la clase de dominio. En nuestro caso realizaremos las Named queries mediante anotaciones en la clase java Car.
– Definir cada uno de esos DAO en el application context de Spring. Para ello tendremos que indicar que sobre que clase, la interfaz del DAO en cuestión, queremos hacer un proxy y cuál es la clase de dominio específica sobre la que va a trabajar el DAO. Para ello tenemos que usar los abstractDaoTarget y abstractDao anteriormente mencionados.

Nuestros fichero de contexto va a tener la siguiente pinta:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
    "http://www.springframework.org/dtd/spring-beans.dtd">

<beans default-autowire="byName">


	<bean id="carDao" parent="abstractDao">
		<property name="proxyInterfaces">
			<value>com.hopcroft.examples.dao.CarDao</value>
		</property>
		<property name="target">
			<bean parent="abstractDaoTarget">
				<constructor-arg>
					<value>com.hopcroft.examples.domain.Car</value>
				</constructor-arg>
			</bean>
		</property>
	</bean>


	<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
		<property name="driverClassName" value="com.mysql.jdbc.Driver" />
		<property name="url" value="jdbc:mysql://localhost/test" />
		<property name="username" value="root" />
		<property name="password" value="" />
		<property name="initialSize" value="5" />
		<property name="maxActive" value="10" />
	</bean>

	<bean id="sessionFactory"
		class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="hibernateProperties">
			<props>
				<prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
				<prop key="hibernate.show_sql">true</prop>
				<prop key="hibernate.hbm2ddl.auto">none</prop>
			</props>
		</property>
		<property name="packagesToScan" value="com.hopcroft.examples.domain" />
	</bean>

	<bean id="finderIntroductionAdvisor" class="com.hopcroft.examples.dao.FinderIntroductionAdvisor" />

	<bean id="abstractDaoTarget" class="com.hopcroft.examples.dao.GenericDaoHibernateImpl"
		abstract="true">
		<property name="sessionFactory">
			<ref bean="sessionFactory" />
		</property>
	</bean>

	<bean id="abstractDao" class="org.springframework.aop.framework.ProxyFactoryBean"
		abstract="true">
		<property name="interceptorNames">
			<list>
				<value>finderIntroductionAdvisor</value>
			</list>
		</property>
	</bean>
</beans>

Si queremos probar nuestro proyecto podemos crear una clase de Junit.

package com.hoppcroft.examples.test;

import java.io.Serializable;
import java.util.List;

import org.junit.Assert;
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 com.hopcroft.examples.dao.CarDao;
import com.hopcroft.examples.domain.Car;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/applicationContext.xml" })
public class CarTest {

	@Autowired
	private CarDao carDao;

	private long nCars;
	private Serializable idCar;

	@Before
	public void initialize() {
		nCars = carDao.findCars().size();
	}

	@Test
	public void crearCar(){
		Car car = new Car();
		car.setId(++nCars);
		car.setCompany("Opel");
		car.setModel("Astra");
		car.setPrice(16000L);
		idCar = carDao.create(car);
		Assert.assertEquals(idCar,nCars);
	}

	@Test
	public void obtenerCar(){
		Car car = carDao.read(1L);
		Assert.assertEquals(1L, car.getId());

	}

	@Test
	public void listarCar(){
		List cars = carDao.findCarsByCompany("Opel");
		Assert.assertNotNull(cars);

	}

}

Podemos ver el resultado del log:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.hoppcroft.examples.test.CarTest
23-jun-2011 0:22:05 org.springframework.test.context.TestContextManager retrieveTestExecutionListeners
INFO: @TestExecutionListeners is not present for class [class com.hoppcroft.examples.test.CarTest]: using defaults.
23-jun-2011 0:22:06 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [applicationContext.xml]
23-jun-2011 0:22:06 org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.GenericApplicationContext@86988: startup date [Thu Jun 23 00:22:06 CEST 2011]; root of context hierarchy
23-jun-2011 0:22:06 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@5bda13: defining beans [carDao,dataSource,sessionFactory,finderIntroductionAdvisor,abstractDaoTarget,abstractDao,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalPersistenceAnnotationProcessor]; root of factory hierarchy
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.
23-jun-2011 0:22:07 org.springframework.orm.hibernate3.LocalSessionFactoryBean buildSessionFactory
INFO: Building new Hibernate SessionFactory
Hibernate: select car0_.id as id0_, car0_.company as company0_, car0_.model as model0_, car0_.price as price0_ from Car car0_
Hibernate: insert into Car (company, model, price, id) values (?, ?, ?, ?)
Hibernate: select car0_.id as id0_, car0_.company as company0_, car0_.model as model0_, car0_.price as price0_ from Car car0_
Hibernate: select car0_.id as id0_0_, car0_.company as company0_0_, car0_.model as model0_0_, car0_.price as price0_0_ from Car car0_ where car0_.id=?
Hibernate: select car0_.id as id0_, car0_.company as company0_, car0_.model as model0_, car0_.price as price0_ from Car car0_
Hibernate: select car0_.id as id0_, car0_.company as company0_, car0_.model as model0_, car0_.price as price0_ from Car car0_ where car0_.company=?
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.727 sec

Results :

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
 
10 comentarios

Publicado por en 23 junio, 2011 en J2EE

 

Etiquetas: , , , ,

10 Respuestas a “Don’t repeat the DAO – No repitas el DAO con Spring e Hibernate

  1. Antonio

    4 julio, 2011 at 8:48 am

    Me ha sido de gran ayuda. Gracias

     
    • hop2croft

      4 julio, 2011 at 8:56 am

      de nada!!. Gracias a ti por visitar el blog.

      Un saludo.

       
  2. Shaddow

    21 julio, 2011 at 6:54 pm

    Lo primero gracias por el tutorial, me está siendo de gran ayuda.

    Una consulta, al ejecutar las operaciones de update o delete no me está funcionando correctamente. Ni está actualizando los objetos ni borrándolos, yo diría que podría tener algo que ver con temas de transacciones, es como si no se estuviera realizando el commit…. además tengo problemas de bloqueos en la bbdd.

    Te paso los test que he añadido:

    @Test
    public void updateCar() {
    Car car = carDao.read(1L);
    car.setModel(«Astra 2011»);
    logger.info(«car.getModel()={}», car.getModel());
    carDao.update(car);

    car = carDao.read(1L);
    logger.info(«car.getModel()={}», car.getModel());
    Assert.assertEquals(«Astra 2011», car.getModel());
    }

    @Test
    public void borrarCar() {
    Car car = carDao.read(1L);
    carDao.delete(car);
    car = carDao.read(1L);
    Assert.assertNull(car);
    }

    ¿Alguna idea de cuál puede ser el problema?

    Gracias!

     
    • hop2croft

      22 julio, 2011 at 9:19 am

      Hola,

      muchas gracias por seguir el blog.

      En principio no haría falta añadir un transaction manager en el contexto de Spring … pero efectivamente los update y los delete no funcionaban del modo que están implementados :(. Para hacerlos funcionar sustituye las líneas de código getSession().delete(o) y getSession().update(o) de los métodos delete y update de GenericDaoHibernateImpl por getHibernateTemplate().update(o) y getHibernateTemplate().delete(o) respectivamente.

      Saludos.

       
  3. Shaddow

    22 julio, 2011 at 1:47 pm

    Muchas gracias, con ese cambio funciona perfectamente 😀

     
  4. jamileth ramirez

    14 marzo, 2012 at 9:20 pm

    Gracias buen aporte, una duda disculpa no encuentro la interfas FinderExecutor me puedes decir como hacerla, gracias disculpa la molestia, soy novato en esto y esta bien esplicado

     
    • hop2croft

      15 marzo, 2012 at 12:32 am

      Hola Jamileth,

      gracias por leer el blog, me alegro que te esté sirviendo de ayuda. La interfaz FinderExecutor la puedes obtener agregando la siguiente dependencia en tu pom.xml.

      groupId: org.codehaus.openxma
      artifactId: dsl-platform
      version: 4.1.3

      Una vez añadida en tu proyecto la tendrás que implementar en tu clase GenericDAO*** tal y como indica el ejemplo del post.

      En cualquier caso, una página que me sirve cuando me enfrento a que no sé donde está la dependencia de una determinada clase es mvn repository o jarvana. Estas dos páginas tienen buscadores que permiten indicar un clase y te dicen que proyecto la implementa. Además te permiten bajar el jar asociado o bien te dicen como la tienes que agregar en tu fichero pom. En el caso del FinderExecutor puedes verla aquí.

      Un saludo y gracias de nuevo por leer el blog,
      I.

       

Deja un comentario