miércoles, 15 de mayo de 2013

Sustituyendo la capa de persistencia de Spring JDBC por Spring + Hibernate.

Hola a todos.

En esta entrada llevaremos la serie Introducción a Spring JDBC como solución a los problemas de uso e JDBC en proyectos productivos, que hasta la última entrega iba por la entrada 3 a un nuevo nivel sustituyendo la capa de persistencia de Spring JDBC por la combinación de Spring + Hibernate3 y anotaciones propias de Hibernate

La entrada está un poco extensa porque incluiré el código necesario para explicar los cambios.

El diagrama que representa lo que queremos hacer es el siguiente:

clip_image002

Para comenzar haremos una copia del proyecto anterior cambiándole el nombre en el mismo IDE STS de Spring.

clip_image003

Luego comenzaremos a trabajar con las interfaces. Para este caso implementaremos una interfaz genérica que nos servirá para todos los futuros DAO que tengamos que implementar de cualquier entidad del negocio.

Esta interfaz se verá así:

package org.ejemplos.blog.spring.Interfaces;

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

import org.hibernate.criterion.DetachedCriteria;

public interface BaseDao<T extends Serializable, E> {
public void deleteAll(Collection<T> instances) throws Exception;

public int bulkUpdate(String query) throws Exception;

public E save(T instance) throws Exception;

public void saveOrUpdateAll(Collection<T> instances) throws Exception;

public void saveOrUpdate(T instance) throws Exception;

public void persist(T transientInstance) throws Exception;

public void attachDirty(T instance) throws Exception;

public void attachClean(T instance) throws Exception;

public void delete(T persistentInstance) throws Exception;

public List<T> findByExample(T instance) throws Exception;

public List<T> findByQuery(String query) throws Exception;

public List<Map<String, Object>> findMapByQuery(String queryString)
throws Exception;

public List<T> findByCriteria(DetachedCriteria criteria) throws Exception;

public T merge(T detachedInstance) throws Exception;

public List<T> findAll() throws Exception;

public T findById(E id) throws Exception;
}

 


Con ayuda de la genericidad no necesitamos especificar el tipo de clase que usará la implementación de esta interface. Luego veremos como se usa y la utilidad que tiene. Solo decir que la interface está parametrizada por una clase genérica, T, y una clave primaria que a su vez es serializable. Esto nos permite que independientemente del tipo de nuestra clase podamos acceder de forma segura a sus métodos.

Estos métodos identificados en la interface nos permiten realizar casi cualquier operación sobre las entidades, es por eso que teniéndolos ya especificamos pues se nos hace más fácil la implementación.

El otro cambio que debemos realizar es en el pom.xml del proyecto para que Maven nos cargue las nuevas dependencias de spring + hibernate, quedaría de la siguiente manera:

<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>cu.casa.jorge.spring</groupId>
<artifactId>ejemplo-dao-spring-hibernate</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>ejemplo-dao-spring-hibernate</name>
<url>http://maven.apache.org</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<org.springframework.version>3.2.2.RELEASE</org.springframework.version>
<org.spring-test.version>3.2.0.RELEASE</org.spring-test.version>
<postgresql.version>9.1-901.jdbc4</postgresql.version>
<junit.version>4.11</junit.version>
<commons-dbcp.version>1.2.2</commons-dbcp.version>
<hibernate.version>3.5.0-Final</hibernate.version>
</properties>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${org.springframework.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${org.spring-test.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${org.springframework.version}</version>
</dependency>
<dependency>
<groupId>postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.version}</version>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>${commons-dbcp.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-annotations</artifactId>
<version>${hibernate.version}</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.1</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.0.0.GA</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>4.2.0.Final</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>1.0.0.Final</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>

<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.4.3</version>
<executions>
<execution>
<id>default-test</id>
<phase>test</phase>
<goals>
<goal>test</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>

</build>
</project>

 


Seguimos con los cambios y entonces crearemos la interfaz para el futuro DAO que implementaremos, para seguir la idea de las entradas anteriores el DAO se usará con la entidad Capitulo así que la interface ICapitulo quedará así:

package com.test.maven.Interfaces;

import java.util.List;
import com.test.maven.Dominio.Capitulo;

public interface ICapitulo extends BaseDao<Capitulo, String> {

}

 


Como ven es un cambio significativo, ya aquí no tenemos que declarar nuevos métodos pues los extendemos de BaseDao. Es importante señalar que podríamos haber seguido con la interface inicial, pero creo que esta es mucho mejor y permitirá implementar más funcionalidades sin tener que realizarle modificaciones en un futuro.

Fíjense como cuando extendemos de BaseDao le especificamos la clase que se le debe pasar “Capitulo” y que el ID será de tipo String.

Seguimos con los cambios y nos vamos entonces a la clase entidad Capitulo, porque por la implementación que tiene del ejemplo anterior nos da un error en la interface que acabamos de crear.

Lo primero es hacer que la clase implemente la interface Serializable y luego agregarle anotaciones a la misma clase y a sus atributos. Quedando entonces de la siguiente manera:

package com.test.maven.Dominio;

import java.io.Serializable;

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

@Entity(name = "Capitulo")
@Table(name = "capitulo")
public class Capitulo implements Serializable {

private static final long serialVersionUID = 7907514315633607867L;
@Id
@Column(name = "idx", length = 100)
private String indice;

@Column(name = "titulo", length = 100, nullable = false)
private String titulo;

@Column(name = "numero_paginas", length = 100, nullable = false)
private int numero_paginas;

@Column(name = "isbn_libro", length = 100, nullable = false)
private String isbn_libro;
/*
* Aquí seguirían los métodos get y set que no cambian en nada
*/

 


Las anotaciones que hemos usado son las siguientes:


  • @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.

Ahora comenzaremos con la implementación. Para ello implementaremos una clase que extiende de Serializable, de HibernateDaoSupport (un template para hibernate) y que implementa la interface BaseDao. Esta clase la reutilizo siempre entre proyectos a no ser que haya métodos que deban ser implementados de una forma particular. Por lo que la copio de un proyecto anterior. :-D

Quedaría como sigue:

package com.test.maven.DAOImpl;

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

import com.test.maven.Interfaces.BaseDao;

import org.hibernate.LockMode;
import org.hibernate.criterion.DetachedCriteria;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

public abstract class BaseDaoHibernate<T extends Serializable, E> extends
HibernateDaoSupport implements BaseDao<T, E> {
public void deleteAll(final Collection<T> instances) throws Exception {
try {
getHibernateTemplate().deleteAll(instances);
} catch (final Exception e) {
throw e;
}
}

public int bulkUpdate(final String query) throws Exception {
try {
return getHibernateTemplate().bulkUpdate(query);
} catch (final Exception e) {
throw e;
}
}

@SuppressWarnings("unchecked")
public E save(final T instance) throws Exception {
try {
return (E) getHibernateTemplate().save(instance);
} catch (final Exception e) {
throw e;
}
}

public void saveOrUpdateAll(final Collection<T> instances) throws Exception {
try {
getHibernateTemplate().saveOrUpdateAll(instances);
} catch (final Exception e) {
throw e;
}
}

public void saveOrUpdate(final T instance) throws Exception {
try {
getHibernateTemplate().saveOrUpdate(instance);
} catch (final Exception e) {
throw e;
}
}

public void persist(final T transientInstance) throws Exception {
try {
getHibernateTemplate().persist(transientInstance);
} catch (final Exception e) {
throw e;
}
}

public void attachDirty(final T instance) throws Exception {
try {
getHibernateTemplate().saveOrUpdate(instance);
} catch (final Exception e) {
throw e;
}
}

public void attachClean(final T instance) throws Exception {
try {
getHibernateTemplate().lock(instance, LockMode.NONE);
} catch (final Exception e) {
throw e;
}
}

public void delete(final T persistentInstance) throws Exception {
try {
getHibernateTemplate().delete(persistentInstance);
} catch (final Exception e) {
throw e;
}
}

public T merge(final T detachedInstance) throws Exception {
try {
final T result = getHibernateTemplate().merge(detachedInstance);
return result;
} catch (final Exception e) {
throw e;
}
}

@SuppressWarnings("unchecked")
public List<T> findByExample(final T instance) throws Exception {
try {
final List<T> results = getHibernateTemplate().findByExample(
instance);
return results;
} catch (final Exception e) {
throw e;
}
}

@SuppressWarnings("unchecked")
public List<T> findByQuery(final String queryString) throws Exception {
try {
final List<T> results = getHibernateTemplate().find(queryString);
return results;
} catch (final Exception e) {
throw e;
}
}

@SuppressWarnings("unchecked")
public List<Map<String, Object>> findMapByQuery(final String queryString)
throws Exception {
try {
final List<Map<String, Object>> results = getHibernateTemplate()
.find(queryString);
return results;
} catch (final Exception e) {
throw e;
}
}

@SuppressWarnings("unchecked")
public List<T> findByCriteria(final DetachedCriteria criteria)
throws Exception {
try {
return getHibernateTemplate().findByCriteria(criteria);
} catch (final Exception e) {
throw e;
}
}

public abstract List<T> findAll() throws Exception;

public abstract T findById(E id) throws Exception;
}

 


Como puede ver casi todos los métodos lo que hacen es llamar al método correspondiente de getHibernateTemplate, que es el template que estamos usando, con excepción de los 2 últimos métodos que vamos a implementar. Esto es a manera de ejemplo porque podríamos brindar la misma funcionalidad con los métodos que ya tenemos.

La implementación del DAO de Capitulo quedaría como sigue:

package com.test.maven.DAOImpl;

import java.util.List;

import com.test.maven.Dominio.Capitulo;
import com.test.maven.Interfaces.ICapitulo;

public class CapituloDAO extends BaseDaoHibernate<Capitulo , String> implements ICapitulo{

@Override
public List<Capitulo> findAll() throws Exception {

return getHibernateTemplate().loadAll(Capitulo.class);

}

@Override
public Capitulo findById(String id) throws Exception {
Capitulo capitulo = getHibernateTemplate().get(Capitulo.class, id);
return capitulo;
}

}

 


Y llegados aquí es importante decir que esta es la clase que tendremos que implementar realmente para el resto de las entidades en nuestra aplicación, ya que casi todo lo demás es reutilizable. Mediante la genericidad, especificamos que nuestro DAO es sobre la entidad Capitulo y su id es de tipo String

Como ven solo tengo que implementar los métodos que no implementé en BaseDaoHibernate.

Siguiendo con los cambios vamos a modificar el fichero de configuración de spring que usaremos para las pruebas quedando como sigue:

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


<context:component-scan base-package="com.test.maven" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="org.postgresql.Driver" />
<property name="url" value="jdbc:postgresql://localhost:5432/BDEjemplo" />
<property name="username" value="postgres" />
<property name="password" value="descarga" />
</bean>


<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="packagesToScan" value="com.test.maven" />
<property name="hibernateProperties">
<props>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.dialect">org.hibernate.dialect.PostgreSQLDialect</prop>
<prop key="hibernate.hbm2ddl.auto">update</prop>
</props>
</property>
</bean>

<bean id="hibernateTemplate" class="org.springframework.orm.hibernate3.HibernateTemplate">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<bean id="CapituloDAO" class="com.test.maven.DAOImpl.CapituloDAO">
<property name="hibernateTemplate" ref="hibernateTemplate" />
</bean>

</beans>

 


Lo importante aquí es la creación del bean sessionFactory que será de tipo AnnotationSessionFactoryBean porque estamos usando anotaciones mediante JPA donde indicamos el bean que nos cargará la fuente de datos usando el atributo ref que nos permite referenciar otros objetos dentro del mismo contexto, los paquetes a escanear y de esa manera no tendremos que indicar las clases a buscar, y le pasamos varias propiedades al hibernate para que nos muestre las consultas que se ejecuten, le especificamos el dialecto, algo importante para que las consultas se construyan bien porque como habrán visto no hemos escrito ninguna consulta, y finalmente especificamos que la estructura de la base de datos se actualice ante cualquier cambio de nuestras clases entidad. El otro bean que creamos es el hibernateTemplate al que le inyectamos el sessionFactory anterior y por último el bean CapituloDAO al que le inyectamos el bean anterior y listo.

Recuerden que en otra entrada explicaré el mecanismo detrás de todas estas cosas nuevas, de momento basta saber que funcionan así como están. :-D

Por último nos vamos a la clase de prueba que queda como sigue:

package com.test.maven;

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.test.maven.DAOImpl.CapituloDAO;
import com.test.maven.Dominio.Capitulo;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("spring-context.xml")
public class CapituloDAOTest {
@Autowired
private CapituloDAO capituloDAO;

public void setCapituloDao(CapituloDAO capituloDao) {
this.capituloDAO = capituloDao;
}

@Before
public void setUp() throws Exception {
}

@Test
public void testFindCapitulobyID() throws Exception {

Assert.isInstanceOf(Capitulo.class, capituloDAO.findById("23"));

}
@Test
public void testRetornaCapitulo() throws Exception{

Capitulo capitulo = capituloDAO.findById("23");
Assert.notNull(capitulo);
}
@Test
public void testListadoCapitulos() throws Exception{

Assert.notEmpty(capituloDAO.findAll());
}
}

 


Cuando corremos esta clase con mvn test o en el STS dando clic derecho en el proyecto Run As/ Maven test, veremos lo siguiente en la consola:

-------------------------------------------------------

T E S T S

-------------------------------------------------------

Running com.test.maven.CapituloDAOTest

may 03, 2013 6:43:50 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions

Información: Loading XML bean definitions from class path resource [com/test/maven/spring-context.xml]

may 03, 2013 6:43:50 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh

Información: Refreshing org.springframework.context.support.GenericApplicationContext@2cf0bca1: startup date [Fri May 03 18:43:50 EDT 2013]; root of context hierarchy

may 03, 2013 6:43:50 PM org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons

Información: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@a39e3dd: defining beans [editoraDAO,libroDAO,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,dataSource,sessionFactory,hibernateTemplate,CapituloDAO,org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor]; 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.

may 03, 2013 6:43:51 PM org.springframework.orm.hibernate3.LocalSessionFactoryBean buildSessionFactory

Información: Building new Hibernate SessionFactory

Hibernate: select capitulo0_.idx as idx0_0_, capitulo0_.isbn_libro as isbn2_0_0_, capitulo0_.numero_paginas as numero3_0_0_, capitulo0_.titulo as titulo0_0_ from capitulo capitulo0_ where capitulo0_.idx=?

Hibernate: select capitulo0_.idx as idx0_0_, capitulo0_.isbn_libro as isbn2_0_0_, capitulo0_.numero_paginas as numero3_0_0_, capitulo0_.titulo as titulo0_0_ from capitulo capitulo0_ where capitulo0_.idx=?

Hibernate: select this_.idx as idx0_0_, this_.isbn_libro as isbn2_0_0_, this_.numero_paginas as numero3_0_0_, this_.titulo as titulo0_0_ from capitulo this_

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.521 sec

Results :

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

may 03, 2013 6:43:52 PM org.springframework.context.support.AbstractApplicationContext doClose

Información: Closing org.springframework.context.support.GenericApplicationContext@2cf0bca1: startup date [Fri May 03 18:43:50 EDT 2013]; root of context hierarchy

[INFO] ------------------------------------------------------------------------

[INFO] BUILD SUCCESS

[INFO] ------------------------------------------------------------------------

[INFO] Total time: 5.472s

[INFO] Finished at: Fri May 03 18:43:52 EDT 2013

[INFO] Final Memory: 8M/104M

[INFO] ------------------------------------------------------------------------

Como se ve los 3 test pasaron sin problema. Por lo que con seguridad podemos decir que nuestra implementación está correcta.

Ahora si quisiéramos seguir con este proyecto implementando los restantes DAO podríamos reutilizar casi todo el código que hemos escrito lo que incrementaría notablemente nuestra productividad y todo gracias a la combinación de Spring + Hibernate.


¿Te ha gustado este Post? Compártelo con tus amigos.

No hay comentarios:

Publicar un comentario

IconIconIcon