miércoles, 14 de octubre de 2015

Spring - Parte 5: Integración y manejo de transacciones con Hibernate

Spring - Parte 5: Integración y manejo de transacciones con Hibernate

A lo largo de una serie de tutoriales hemos visto las bondades que nos ofrece el framework Spring para el manejo del ciclo de vida de beans, inyección de dependencia y configuración a través de anotaciones y de archivos de configuración en XML.

Además ya hemos visto cómo trabajar con el framework Hibernate para el manejo de persistencia de nuestra aplicación. Este framework nos permite olvidarnos de escribir queries a mano para centrarnos en la lógica y el flujo de la aplicación, además de proporcionar mecanismos de caché de datos.

En este tutorial veremos cómo integrar estos dos frameworks para que Spring se encargue del manejo de las sesiones y las transacciones que Hibernate usa.

Spring soporta integración con varios frameworks de persistencia como Hibernate, Java Persistence API (JPA), Java Data Objects (JDO) e iBatis (ahora myBatis) para realizar la implementación de los DAOs (Data Access Object) y la estrategia de transacciones. Además podemos configurar todas las características que nuestro framework de ORM soporte, a través del mecanismo de inyección de dependencias de Spring.

El framework de Spring agrega mejoras significativas a la capa de ORM de nuestra elección cuando creamos aplicaciones que hacen uso de acceso a datos. Podemos dejarle a Spring tanto del trabajo de integración como queramos, teniendo la tranquilidad de usar un API probada y optimizada. También nos permite configurarlo de distintas maneras y a distintos niveles haciéndolo muy flexible al momento de tener que modificar algún aspecto del manejo o implementación de persistencia y/o transacciones de nuestra aplicación.

Suficiente teoría. Comencemos con la parte práctica del tutorial.

Lo primero que haremos es crear un nuevo proyecto en NetBeans. Para esto vamos al Menú "File->New Project...". En la ventana que se abre seleccionamos la categoría "Java" y en el tipo de proyecto "Java Application". Le damos una ubicación y un nombre al proyecto, en mi caso será "SpringHibernateIntegracion". Nos aseguramos que la opción "Create Main Class" esté habilitada y la damos un nombre (incluyendo los paquetes) a la nueva clase, en mi caso será "com.javatutoriales.spring.integration.hibernate.Main" (lo sé, es un poco larga pero esto sirve para que se más claro lo que estamos haciendo). Presionamos el botón "Finish" y veremos aparecer en el editor nuestra clase "Main".

*Nota: Yo usaré dos proyectos, uno para usar archivos de configuración en XML y otro para usar anotaciones y que el código de ambos no se mezcle.

Agregamos la biblioteca de "Spring4" que creamos en el primer tutorial de la serie de Spring y la biblioteca "Hibernate4" que creamos en el primer tutorial de la serie de Hibernate. También agregaremos el driver de MySQL, a través de la biblioteca "MySQL JDBC Driver" (incluida en el NetBeans).

*Nota: Estamos actualizando los tutoriales de Hibernate para hacer uso de la versión 4, por lo que por ahora sólo deben saber que necesitan los siguientes jars, de la versión 4.2.7 de Hibernateantlr-2.7.7.jardom4j-1.6.1.jarhibernate-commons-annotations-4.0.2.Final.jarhibernate-core-4.2.7.SP1.jarhibernate-jpa-2.0-api-1.0.1.Final.jarjavassist-3.18.1-GA.jarjboss-logging-3.1.0.GA.jarjboss-transaction-api_1.1_spec-1.0.1.Final.jar

Hacemos clic derecho sobre el nodo "Libraries" del proyecto, en el menú que aparece elegimos la opción "Add Library..."



En la ventana que se abre seleccionamos las bibliotecas "Hibernate4", "Spring4" y "MySQL JDBC Driver":



Hasta el momento nuestras bibliotecas deben verse de la siguiente forma:



Adicionalmente debemos agregar los jars con las clases necesarias de Spring para el manejo de persistencia:
  • spring-orm-4.1.3.RELEASE.jar
  • spring-jdbc-4.1.3.RELEASE.jar
  • spring-tx-4.1.3.RELEASE.jar

Y si queremos que Spring maneje nuestras transacciones debemos agregar además los siguientes jars:
  • spring-aop-4.1.3.RELEASE.jar
  • com.springsource.org.aopalliance-1.0.0.jar (que pueden descargar desde java2s).

Nuestras bibliotecas finalmente deben verse así:



Ahora que ya tenemos las librerías, el siguiente paso es crear la base de datos. Usaremos una base de datos en MySQL que se llamara "tutorial_hibernate". En esta base de datos pondremos sólo una tabla que será "CONTACTO". Para crear esta tabla usaremos el siguiente script:

CREATE TABLE contacto (
  ID bigint(20) NOT NULL AUTO_INCREMENT,
  NOMBRE varchar(255) DEFAULT NULL,
  email varchar(255) DEFAULT NULL,
  telefono varchar(255) DEFAULT NULL,
  PRIMARY KEY (ID)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8

También crearemos una clase, cuyas instancias serán guardadas en la tabla anterior. Para esto, primero crearemos un nuevo paquete llamado "entidades", y en este paquete creamos una nueva clase llamada "Contacto", que debe implementar la interface "java.io.Serializable":

public class Contacto implements Serializable{
}

Contacto será una clase muy sencilla que nos servirá para transportar los datos (un TO [Transfer Object] o DTO [Data Transfer Object]) desde y hacia la base de datos, por lo que sólo tendrá unos cuantos atributos, que serán equivalentes a las columnas de la tabla "contacto" que tenemos en la base de datos (también colocaremos getters y setters de estos atributos):

public class Contacto implements Serializable {

    private long id;
    private String nombre;
    private String email;
    private String telefono;

    public long getId() {
        return id;
    }

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

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getTelefono() {
        return telefono;
    }

    public void setTelefono(String telefono) {
        this.telefono = telefono;
    }
}

En el código anterior, seguramente notaron que el método "setId" está declarado como "protected" y no como "public". Esto ocurre porque en este caso no queremos que el usuario pueda establecer el valor del identificador del objeto, queremos que sea la base de datos quien genere este valor y Hibernate quien lo coloque en el objeto. Haciendo el método protegido nos aseguramos de que sólo un objeto que extienda de "Contacto" pueda establecer el valor (la cual, por cierto, es la forma de trabajar de Hibernate).

En la clase "Contacto" pondremos además un par de constructores, uno vacío y otro que reciba todos los atributos (excepto el id). El primer constructor es un requisito de Hibernate y también puede ser "protected" por las mismas razones que expusimos hace un momento, y el segundo es para facilitarnos a nosotros la creación de los objetos.

protected Contacto() {
}

public Contacto(String nombre, String email, String telefono) {
    this.nombre = nombre;
    this.email = email;
    this.telefono = telefono;
}

La clase "Contacto" completa queda de la siguiente forma:

public class Contacto implements Serializable {

    private long id;
    private String nombre;
    private String email;
    private String telefono;

    protected Contacto() {
    }

    public Contacto(String nombre, String email, String telefono) {
        this.nombre = nombre;
        this.email = email;
        this.telefono = telefono;
    }

    public long getId() {
        return id;
    }

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

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getTelefono() {
        return telefono;
    }

    public void setTelefono(String telefono) {
        this.telefono = telefono;
    }
}

Ahora necesitamos una interface que defina las operaciones que podremos realizar en la capa de persistencia, sobre los registros de la tabla "contacto". El crear esta interface nos ayudará a que nuestra aplicación mantenga un bajo acoplamiento, separando el código de lógica de negocio del de acceso a datos, de esta forma podremos cambiar la implementación del DAO siempre que use la misma interface, y nuestra capa de lógica ni siquiera notará el cambio. Crearemos una nueva interface llamada "ContactosDAO", dentro de un paquete "persistencia" de nuestra aplicación. Esta interface tendrá 5 métodos que nos permitirán guardar nuevos Contactos, actualizarlos, eliminarlos, y buscarlos. La interface "ContactosDAO" queda de la siguiente forma:

public interface ContactosDAO {

    void actualizaContacto(Contacto contacto) throws HibernateException;

    void eliminaContacto(Contacto contacto) throws HibernateException;

    long guardaContacto(Contacto contacto) throws HibernateException;

    Contacto obtenContacto(long idContacto) throws HibernateException;

    List<Contacto> obtenListaContactos() throws HibernateException;
}

El siguiente paso es crear una implementación de la interface anterior, que contenga la lógica para conectarse a la base de datos. Como en nuestro caso la implementación será realizada con Hibernate, nos basaremos en lo que aprendimos en la serie de tutoriales deHibernate. Recordemos que para poder hacer operaciones sobre la base de datos lo primero que necesitamos es obtener una instancia de la clase "org.hibernate.SessionFactory". Obtener esta instancia es un trabajo "pesado", esto quiere decir que se necesita algo de tiempo para hacerlo y que consume una cantidad considerable de memoria, por lo tanto la recomendación es que esta instancia sea creada una sola vez, y que sea al inicio de la aplicación.

En los tutoriales de Hibernate lográbamos esto con una clase de utilidad llamada "HibernateUtil", o extendiendo de una clase base "AbstracHibernateDAO", y creábamos la instancia de "SessionFactory" en un bloque de inicialización estática dentro de e esta clase.

En esta ocasión Spring se encargará de crear esta instancia, lo único que tendremos que hacer es configurar algunos datos para la creación de este "SessionFactory", en el archivo de configuración de Spring y este se encargará del resto. Además haremos uso de la inyección de dependencias para poder acceder a la instancia de "SessionFactory" creada por Spring. Veremos cómo hacer esto un poco más adelante en el tutorial.

Creamos una nueva clase, en el paquete "persistencia", llamada "ContactosDAOHibernateImpl". Esta clase implementará la interface "ContactosDAO":

public class ContactosDAOHibernateImpl implements ContactosDAO{
}

Antiguamente Spring proporcionaba "templates" para realizar los queries y mapearlos a objetos de negocio de nuestra aplicación. Sin embargo, ahora la estrategia recomendada es escribir las implementaciones de los DAO usando Hibernate "plano", o sea sin hacer uso de ninguna dependencia de Spring.

Basándonos en la serie de tutoriales de Hibernate, declararemos tres variables de instancia en esta clase. La primera será una instancia de "SessionFactory", que es el objeto que usamos para crear los objetos "org.hibernate.Session". La segunda será una instancia de "Session", que nos proporciona las operaciones para guardar, eliminar y obtener objetos desde y hacia la base de datos; y la tercera será una instancia de "org.hibernate.Transaction", que es un objeto que define una unidad de trabajo en la base de datos.

public class ContactosDAOHibernateImpl implements ContactosDAO {
    
    private Session sesion;
    
    private Transaction transaction;
    
    private SessionFactory sessionFactory;
}

También colocaremos un setter para el "SessionFactory". ¿Por qué? Porque recordemos que en este caso será Spring quien se encargará de crear este objeto, y lo pasará a nuestra clase a través de ese setter:

public class ContactosDAOHibernateImpl implements ContactosDAO {

    private Session sesion;
    private Transaction transaction;
    private SessionFactory sessionFactory;

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }
}

Ahora colocaremos dos métodos privados en esta clase, que nos ayudarán a iniciar una operación, iniciando una sesión y una transacción; y a manejar los errores en caso de una excepción, haciendo un rollback de la transacción. Estos métodos los tomaremosdel tutorial de Hibernate con una ligera modificación.

El primer método se llamará "iniciaOperacion", y se encargará de iniciar una sesión, usando el objeto "SessionFactory"; y de iniciar una transacción:

private void iniciaOperacion() throws HibernateException {
    sesion = sessionFactory.getCurrentSession();
    transaction = sesion.beginTransaction();
}

El segundo se llamará "manejaExcepcion", y nos permitirá hacer un rollback de la transacción en caso de que exista un error, o sea, que una "HibernateException" sea lanzada mientras trabajamos con la base de datos. Este código (aunque es sólo una línea) lo ponemos en este método por si después quisiéramos agregar más operaciones dentro de este método, como por ejemplo mandar el error a una bitácora o imprimir el stacktrace de la excepción.

private void manejaExcepcion(HibernateException he) throws HibernateException {
    transaction.rollback();
}

Ahora colocaremos a implementación de los métodos de la interface "ContactosDAO". Estas implementaciones también las hemos tomado de los tutoriales de Hibernate, por lo que no daremos una explicación de los mismos (además, parece que son bastante intuitivos). La clase "ContactosDAOHibernateImpl" completa, omitiendo los imports, queda de la siguiente forma:

public class ContactosDAOHibernateImpl implements ContactosDAO {

    private Session sesion;
    private Transaction transaction;
    private SessionFactory sessionFactory;

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    @Override
    public long guardaContacto(Contacto contacto) throws HibernateException {
        long id = 0;
        try {
            iniciaOperacion();
            id = (Long) sesion.save(contacto);
            transaction.commit();
        } catch (HibernateException he) {
            manejaExcepcion(he);
            throw he;
        } finally {
            sesion.close();
        }

        return id;
    }

    @Override
    public void actualizaContacto(Contacto contacto) throws HibernateException {
        try {
            iniciaOperacion();
            sesion.update(contacto);
            transaction.commit();
        } catch (HibernateException he) {
            manejaExcepcion(he);
            throw he;
        } finally {
            sesion.close();
        }
    }

    @Override
    public void eliminaContacto(Contacto contacto) throws HibernateException {
        try {
            iniciaOperacion();
            sesion.delete(contacto);
            transaction.commit();
        } catch (HibernateException he) {
            manejaExcepcion(he);
            throw he;
        } finally {
            sesion.close();
        }
    }

    @Override
    public Contacto obtenContacto(long idContacto) throws HibernateException {
        Contacto contacto = null;
        try {
            iniciaOperacion();
            contacto = (Contacto) sesion.get(Contacto.class, idContacto);
        } finally {
            sesion.close();
        }

        return contacto;
    }

    @Override
    public List<Contacto> obtenListaContactos() throws HibernateException {
        List<Contacto> listaContactos = null;

        try {
            iniciaOperacion();
            listaContactos = sesion.createQuery("from Contacto").list();
        } finally {
            sesion.close();
        }

        return listaContactos;
    }

    private void iniciaOperacion() throws HibernateException {
        sesion = sessionFactory.getCurrentSession();
        transaction = sesion.beginTransaction();
    }

    private void manejaExcepcion(HibernateException he) throws HibernateException {
        transaction.rollback();
    }
}

Ahora crearemos el archivo de configuración de Spring. En este archivo declararemos los beans que Spring usará para crear la conexión hacia la base de datos, el objeto sessionFactory, y para indicar las clases que Hibernate usará como entidades. En este momento colocaremos sólo los elementos esenciales, y los demás los dejaremos pendientes por un momento, ya que algunas cosas dependen de si estamos trabajando con anotaciones o con archivos de mapeo.

Para crear este archivo hacemos clic derecho sobre el nodo "Source Packages" de nuestro proyecto, y luego en "New" -> "SpringXMLConfig":



Si no les aparece esta opción (como es mi caso), seleccionen la opción "Other...", y en la ventana que aparece seleccionen "Other" como categoría, y "SpringXMLCongfig" como tipo de archivo.

A este archivo le daremos "applicationContext" como nombre (NetBeans se encargará de poner la extensión .xml). Antes de crear el archivo NetBeans preguntará los namespaces de Spring que queremos incluir en el archivo. Recodemos que un namespace nos permite separar elementos de configuración de un módulo de Spring de los elementos de otro módulo, de forma que sea más claro lo que estamos configurando. Nosotros seleccionaremos los namapaces "aop", "context" y "tx" (opcionalmente puede seleccionar también el namespace "jee" si van a trabajar con JNDI), los cuales permiten configurar los elementos de programación orientada a aspectos (hablaremos de esto más adelante), escaneo y configuración de componentes, y manejo de transacciones, respectivamente:



Hacemos clic en el botón "Finish", con lo que deberemos de ver nuestro archivo de configuración, el cual tiene más o menos el siguiente contenido:

<?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:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
          http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd
          http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
          http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd">

</beans>

Lo primero que haremos es crear el dataSource, o la conexión hacia la base de datos.

Spring ofrece varias opciones para configurar esta fuente de datos, entre las que se incluyen:
  • Data sources a través de JNDI
  • Data sources a través de drivers JDBC
  • Data sources que usan un pool de conexiones

Obviamente hay algunas diferencias en las características de cada uno de estos tipos de data sources.

Si queremos simular un ambiente real de producción, o los parámetros de nuestra conexión cambiarán dependiendo del ambiente en el que estemos (por ejemplo: desarrollo, pruebas, producción) o estamos desarrollando un proyecto que será instalado en múltiples clientes, o simplemente no tenemos los datos de la conexión, pero nos será proporcionada a través de JNDI (por ejemplo si instalamos nuestra aplicación en un servidor de aplicaciones), lo más recomendable es usar un data source a través de JNDI.

El elemento "<jee:jndi-lookup>" permite recuperar cualquier objeto, incluyendo data sources, de JNDI y ponerlo disponible como un bean de Spring. El atributo "jndi-name", de este elemento, se usa para especificar el nombre del recurso JNDI. Finalmente, si la aplicación se ejecuta en un servidor de aplicaciones, podemos establecer la propiedad "resource-ref" a "true", con lo que el nombre del recurso será precedido con "comp/env/".

Por ejemplo, si tenemos un recurso de tipo data source, cuyo nombre es "conexionTutorial", entonces haremos referencia a este recurso de la siguiente forma:

<jee:jndi-lookup id="dataSource" jndi-name="/jdbc/conexionTutorial" resource-ref="true" />

En caso de que no estemos trabajando con JNDI la siguiente mejor opción es usar un data source que mantenga un pool de conexiones, y declarar los datos para esta conexión directamente en Spring. La forma más común de hacer esto es usando el proyecto Data Base Connection Pool de Apache. Aunque tiene varias clases para proporcionar el pooling, la que se usa con más frecuencia es "BasicDataSource".

Para crear este data source, declaramos un bean de Spring, y establecemos los atributos necesarios para la conexión, como el nombre del driver a usar; la URL, username y password de la conexión; y el tamaño inicial del pool:

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

Para ver qué otros atributos soporta esta bean, pueden revisar la documentación de la clase "BasicDataSource".

Si no necesitamos cosas tan "sofisticadas" como un pool de conexiones o si, como en nuestro caso, estamos haciendo sólo un ejemplo sencillo, podemos usar un driver JDBC como data source. Spring viene con dos clases que pueden servirnos de data source:
  • DriverManagerDataSource. Regresa una nueva conexión cada vez que una conexión es requerida.
  • SingleConnectionDataSource. Regresa la misma conexión cada vez que una conexión es requerida.
La configuración de ambas clases es igual. En nuestro caso elegiremos la clase "DriverManagerDataSource". Aquí nuevamente crearemos un bean de Spring y estableceremos la información requerida para la conexión a la base de datos (como el nombre del driver a usar; la URL, username y password de la conexión):

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost/tutorial_hibernate" />
    <property name="username" value="user" />
    <property name="password" value="password" />
</bean>

Nosotros elegiremos trabajar con este último data source, por lo que hasta ahora nuestro archivo de configuración de Spring (omitiendo los namespaces) se ve de la siguiente forma:

<beans>
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost/tutorial_hibernate" />
        <property name="username" value="user" />
        <property name="password" value="password" />
    </bean>
</beans>

El siguiente paso es crear el SessionFactory que, como recordaremos, nos permitirá crear objetos "Session" con los que interactuaremos con la base de datos. Para crear este objeto nuevamente declararemos un bean de Spring. La clase que usemos dependerá del framework ORM que estemos usando. Como en nuestro caso usamos Hibernate 4, la clase que tenemos que usar es "org.springframework.orm.hibernate4.LocalSessionFactoryBean":

<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
</bean>

Dentro de este bean podemos configuraremos algunos parámetros del framework ORM que estemos usando, en este caso Hibernate. Aquí pondremos parámetros que normalmente colocamos en el archivo de configuración de Hibernate ("hibernate.cfg.xml"), junto con algunos otros datos.

Alguna de esta información dependerá si estamos trabajando con archivos de configuración en XML, o si estamos trabajando con anotaciones. Veremos la configuración común para ambas opciones y un poco más adelante veremos los elementos particulares para cada forma de trabajo.

Lo primero que debemos hacer es indicar el data source que se usará para conectarse a la base de datos, para ello usamos la propiedad "dataSource" y hacemos referencia al bean "dataSource" que creamos hace un momento:

<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="dataSource">
        <ref bean="dataSource"/>
    </property>
</bean>

Lo siguiente es configurar las propiedades de Hibernate que no tienen que ver con la conexión a la base de datos, como por ejemplo pool de conexiones, caché, el dialecto que se usará, los archivos de mapeo o clases anotadas que representan las entidades, etc. De hecho, si ya contamos con un archivo "hibernate.cfg.xml" o simplemente queremos apartar esta información del archivo de configuración de Spring (por ejemplo si queremos que esta configuración pueda ser modificada por otra persona o no queremos llenar este archivo con mucha información), este bean nos permite hacer referencia a un archivo de configuración de Hibernate del cual leerá estos parámetros. Para esto usamos la propiedad "configLocation", al cual le pasamos una lista de ubicaciones de los archivos que utilizará, en este caso sólo tendremos uno, por lo que la propiedad queda de esta forma:

<property name="configLocation">
    <value>classpath:hibernate.cfg.xml</value>
</property>

Creamos este archivo, en la raíz del nodo "Source Packages" del proyecto, como un documento XML:



En este archivo pondremos la información del dialecto de la base de datos, indicaremos que no queremos que se muestre el SQLgenerado por Hibernate, y que la base de datos debe eliminarse ni actualizarse cada vez que hagamos una conexión. El archivo hasta ahora tiene el siguiente contenido:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
  "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
  "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
        <!-- Dialecto de la base de datos -->
        <property name="dialect">org.hibernate.dialect.MySQL5Dialect</property>
    
        <!-- Otras propiedades importantes -->
        <property name="show_sql">false</property>
        <property name="hbm2ddl.auto">none</property>
    
    </session-factory>
</hibernate-configuration>

Lo siguiente dependerá de si estamos usando archivos de mapeo en XML o anotaciones. En el primer caso debemos indicar dónde se encuentra el archivo de mapeo de la clase "Contacto" (archivo que no hemos creado todavía). Supongamos que lo hemos puesto en un directorio especial para los mapeos, debajo del paquete de las entidades, entonces tendremos que usar el atributo "resource" del elemento "mapping" de la siguiente forma:

<mapping resource="com/javatutoriales/spring/integration/hibernate/entidades/mapeos/Contacto.hbm.xml"/>

Si por el contrario, estuviéramos usando anotaciones, deberemos usar el atributo "class" del elemento "mapping", haciendo referencia a la clase "Contacto", de esta forma:

<mapping class="com.javatutoriales.spring.integration.hibernate.entidades.Contacto"/>

El archivo de configuración final de Hibernate depende entonces de si usamos anotaciones o archivos de mapeo. El archivo de configuración de Spring hasta ahora se ve de la siguiente forma:

<?xml version="1.0" encoding="UTF-8"?>
<beans>

    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost/tutorial_hibernate" />
        <property name="username" value="user" />
        <property name="password" value="password" />
    </bean>

    <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
 
        <property name="dataSource">
            <ref bean="dataSource"/>
        </property>
        
        <property name="configLocation">
            <value>classpath:hibernate.cfg.xml</value>
        </property>
    
        <!-- mapping class="com.javatutoriales.spring.integration.hibernate.entidades.Contacto"/-->
        <mapping resource="com/javatutoriales/spring/integration/hibernate/entidades/mapeos/Contacto.hbm.xml"/>
    
    </bean>
</beans>

Ahora, ¿qué pasa si no queremos poner esta información en el archivo de Hibernate, sino directamente en el archivo de Spring? Para eso podemos usar la propiedad "hibernateProperties" del bean "sessionFactory". En esta propiedad colocaremos un conjunto de, valga la redundancia, propiedades que indican los valores de las (si, es la tercera vez) propiedades de Hibernate. Para reproducir lo que teníamos en el archivo de configuración de Hibernate, debemos colocar las propiedades de esta forma:

<property name="hibernateProperties">
    <props>
        <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop>
        <prop key="hibernate.show_sql">true</prop>
    </props>
</property>

Los más observadores se habrán percatado de que aún no hemos colocado la información de en dónde se encuentran las entidades (o los archivos de mapeos de las mismas) que serán manejadas por Hibernate. Para esto tenemos cuatro maneras, las primeras dos para cuando trabajamos con archivos de mapeo en XML.

La primera de las formas consiste en indicar, a través de la propiedad "mappingResources" del bean "sessionFactory", donde se encuentra cada uno de los archivos de mapeo de nuestras entidades, a través de una lista de valores; de la siguiente forma:

<property name="mappingResources">
    <list>                        
        <value>com/javatutoriales/spring/integracion/mapeos/Contacto.hbm.xml</value>
    </list>
</property> 

Esto está bien si tenemos pocas entidades pero en aplicaciones de tamaño mediano a grande este no será el caso, y no sólo tener colocar todos los archivos de mapeo puede ser tardado, sino que el tener que estar quitando los de las entidades que quitamos del diseño de nuestro sistema, o tener que recordar agregar los nuevos, puede ser una labor propensa a errores. Debido a esto les recomiendo el uso de otra propiedad del bean "sessionFactory", llamada "mappingDirectoryLocations" (la cual es la segunda forma de indicar dónde están nuestras entidades). En esta propiedad lo único que debemos hacer es indicar los lugares en los que se encuentran los archivos de mapeo, y digo lugares por si tenemos nuestros archivos de mapeo esparcidos por varios lugares de nuestra aplicación (lo cual no debería de pasar), la propiedad de configura de la siguiente forma:

<property name="mappingDirectoryLocations">
    <list>
        <value>classpath:com/javatutoriales/spring/integracion/mapeos/</value>
    </list>
</property>

Y la configuración completa de nuestro bean "sessionFactory" se ve de la siguiente forma:

<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="dataSource">
        <ref bean="dataSource"/>
    </property>        
    
    <property name="hibernateProperties">
        <props>
            <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop>
            <prop key="hibernate.show_sql">true</prop>
        </props>
    </property>
 
    <property name="mappingDirectoryLocations">
        <list>        
            <value>classpath:com/javatutoriales/spring/integracion/mapeos/</value>
        </list>
    </property>
</bean>

La tercera forma de configurar la ubicación de nuestras entidades manejadas por Hibernate es para cuando anotamos estas entidades. Podemos usar la propiedad "annotatedClasses" para indicar cada una de las clases que están anotadas como entidades, de la siguiente manera:

<property name="annotatedClasses">
    <list>            
        <value>com.javatutoriales.spring.integracion.entidades.Contacto</value>
    </list>
</property> 

Aquí tendremos el mismo problema que cuando indicamos los archivos de mapeo uno por uno, por lo que les recomiendo que usen la cuarta forma, que es a través de la propiedad "packagesToScan" del bean "sessionFactory", en la cual indicamos la lista de los paquetes en los que se encuentran nuestras entidades, de la siguiente manera:

<property name="packagesToScan">
    <list>
        <value>com.javatutoriales.spring.integracion.entidades</value>
    </list>
</property>

Por lo que la configuración del bean "sessionFactory" queda de la siguiente forma si trabajamos con anotaciones:

<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
 
    <property name="dataSource">
        <ref bean="dataSource"/>
    </property>
            
    <property name="hibernateProperties">
        <props>
            <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop>
            <prop key="hibernate.show_sql">true</prop>
            <prop key="hibernate.hbm2ddl.auto">none</prop>
        </props>
    </property>
 
    <property name="packagesToScan">
        <list>
            <value>com.javatutoriales.spring.integration.hibernate.entidades</value>
        </list>
    </property>
 
</bean>

Y así si trabajamos con archivos de mapeo:

<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
 
    <property name="dataSource">
        <ref bean="dataSource"/>
    </property>
                    
    <property name="hibernateProperties">
        <props>
            <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop>
            <prop key="hibernate.show_sql">true</prop>
            <prop key="hibernate.hbm2ddl.auto">none</prop>
        </props>
    </property>
 
    <property name="mappingDirectoryLocations">
        <list>
            <value>classpath:com/javatutoriales/spring/integration/hibernate/entidades/mapeos</value>
        </list>
    </property>
</bean>

Ahora que tenemos la estructura básica para los ejemplos dividiremos este tutorial en dos partes. Recordemos que cada uno de estos frameworks tiene dos formas de ser configurados: con archivos de configuración en XML y con Anotaciones. En la primer parte del tutorial veremos sólo la parte de trabajo con archivos de configuración en XML, y en la segunda parte veremos cómo trabajar con anotaciones. Pero recuerden que pueden usar ambas formas de trabajo mezcladas a su conveniencia.


1.1 INTEGRACIÓN USANDO ARCHIVOS DE CONFIGURACIÓN EN XML

Lo primero que haremos es crear el archivo de mapeo de Hibernate para la entidad "Contacto".

Crearemos un nuevo paquete llamado "mapeos" debajo del paquete de entidades quedando el paquete "com.javatutoriales.spring.integration.hibernate.entidades.mapeos". Dentro de este nuevo paquete creamos un nuevo archivo XML llamado "Contacto.hbm" (NetBeans agregará automáticamente la extensión .xml):



Este mapeo será igual al que tenemos en el tutorial de Hibernate:

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
    <class name="com.javatutoriales.spring.integration.hibernate.entidades.Contacto" table="CONTACTOS">
        <id name="id" column="ID">
            <generator class="identity" />
        </id>
        <property name="nombre" type="string" column="NOMBRE" />
        <property name="email" />
        <property name="telefono" />
    </class>
</hibernate-mapping>

Ahora debemos colocar en el archivo de configuración de Spring un bean para poder obtener una instancia de nuestra clase "ContactosDAOHibernateImpl":

<bean id="contactoDAO" class="com.javatutoriales.spring.integration.hibernate.persistencia.ContactosDAOHibernateImpl" />

Como este bean hace uso de una instancia de "SessionFactory" para el manejo de persistencia (y lo recibe a través del getter que le colocamos) debemos pasarle la instancia del bean "sessionFactory" usando el elemento "property":

<bean id="contactoDAO" class="com.javatutoriales.spring.integration.hibernate.persistencia.ContactosDAOHibernateImpl">
    <property name="sessionFactory" ref="sessionFactory" />
</bean>

El último paso es colocar en nuestra clase "Main" el código para obtener el bean "ContactoDAO" y hacer uso de este para guardar y recuperar un Contacto de la base de datos. Para obtener los beans de Spring haremos uso de la interface "ApplicationContext" y de la clase "ClassPathXmlApplicationContext" como vimos en el segundo tutorial de Spring. El resto del código es bastante simple, sólo usamos los métodos "guardaContacto" y "obtenContacto" para guardar y recuperar una instancia de Contacto y luego sólo imprimimos el nombre y el email del Contacto en la consola:

public static void main(String[] args) throws Exception {
    ApplicationContext appContext = new ClassPathXmlApplicationContext("applicationContext.xml");
    ContactosDAO contactosDAO = appContext.getBean(ContactosDAO.class);        
    long idContactoGuardado = contactosDAO.guardaContacto(new Contacto("Nombre 1", "Email 1", "Telefono 1"));
        
    Contacto contactoGuardado = contactosDAO.obtenContacto(idContactoGuardado);
        
    System.out.println("Nombre: " + contactoGuardado.getNombre());
    System.out.println("Email: " + contactoGuardado.getEmail());
}

Al ejecutar el código debemos ver la siguiente salida en consola (después de quitar algunos de los mensajes de Hibernate):

Hibernate: insert into CONTACTOS (NOMBRE, email, telefono) values (?, ?, ?)
Hibernate: select contacto0_.ID as ID1_0_0_, contacto0_.NOMBRE as NOMBRE2_0_0_, contacto0_.email as email3_0_0_, contacto0_.telefono as telefono4_0_0_ from CONTACTOS contacto0_ where contacto0_.ID=?
Nombre: Nombre 1
Email: Email 1

Con esto comprobamos que la aplicación funciona correctamente. Ahora veamos cómo hacer lo mismo mediante anotaciones.


1.2 INTEGRACIÓN USANDO ANOTACIONES

En este caso hay que colocar las anotaciones de Hibernate en la entidad Contacto. Nuevamente, estas anotaciones serán las mismas que vimos en el segundo tutorial de Hibernate:

@Entity
@Table(name="contacto")
public class Contacto implements Serializable
{
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long id;
    private String nombre;
    
    @Column(name="e_mail")
    private String email;
    private String telefono;

    public Contacto()
    {
    }

    public Contacto(String nombre, String email, String telefono)
    {
        this.nombre = nombre;
        this.email = email;
        this.telefono = telefono;
    }

    public String getEmail()
    {
        return email;
    }

    public void setEmail(String email)
    {
        this.email = email;
    }

    public long getId()
    {
        return id;
    }

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

    public String getNombre()
    {
        return nombre;
    }

    public void setNombre(String nombre)
    {
        this.nombre = nombre;
    }

    public String getTelefono()
    {
        return telefono;
    }

    public void setTelefono(String telefono)
    {
        this.telefono = telefono;
    }
}

Ahora hay que agregar un par de anotaciones en la clase "ContactosDAOHibernateImpl". La primera es para indicarle a Spring que esta será una clase que debe manejar como un bean y que el propósito del bean es realizar acceso a datos, para esto usamos la anotación "@Repository" del paquete "org.springframework.stereotype" a nivel de clase:

@Repository
public class ContactosDAOHibernateImpl implements ContactosDAO {
...
}

Adicionalmente hay que colocar la anotación "@Autowired" en el atributo de tipo "SessionFactory" para indicar que este atributo deberá ser inyectado automáticamente por Spring:

@Autowired
private SessionFactory sessionFactory;

El último paso es indicar en al archivo de configuración de Spring que debe realizar la configuración mediante anotaciones y en qué paquetes se encuentran las clases en las que debe buscar anotaciones. Esto lo hacemos con los siguiente elementos en el archivo "applicationContext":

<context:annotation-config/>

<context:component-scan base-package="com.javatutoriales.spring.integration.hibernate.persistencia" />

Ahora solo falta agregar el mismo códigode antes en nuestra clase Main si no lo hemos hecho aún:

public static void main(String[] args) throws Exception {
    ApplicationContext appContext = new ClassPathXmlApplicationContext("applicationContext.xml");
    ContactosDAO contactosDAO = appContext.getBean(ContactosDAO.class);

    long idContactoGuardado = contactosDAO.guardaContacto(new Contacto("Nombre 1", "Email 1", "Telefono 1"));

    Contacto contactoGuardado = contactosDAO.obtenContacto(idContactoGuardado);

    System.out.println("Nombre: " + contactoGuardado.getNombre());
    System.out.println("Email: " + contactoGuardado.getEmail());
}

Y si todo está bien al ejecutar nuestra aplicación debemos la siguiente salida en la consola:

Hibernate: insert into contactos (e_mail, nombre, telefono) values (?, ?, ?)
Hibernate: select contacto0_.id as id1_0_0_, contacto0_.e_mail as e_mail2_0_0_, contacto0_.nombre as nombre3_0_0_, contacto0_.telefono as telefono4_0_0_ from contactos contacto0_ where contacto0_.id=?
Nombre: Nombre 1
Email: Email 1

Con lo que comprobamos que todo ha funcionado correctamente. El siguiente paso ahora que ya logramos la conexión con la base de datos es manejar las transacciones de la aplicación desde Spring y no desde Hibernate como lo hemos estado haciendo.


2. Manejo de transacciones con Spring

Tradicionalmente las transacciones son la parte más "complicada" y obscura de manejar cuando trabajamos con base de datos y sin embargo es la más importante para mantener la consistencia de información y a la que menos atención le ponemos. Recordemos que una transacción es una secuencia de acciones que son tratadas como una única unidad de trabajo. Estas acciones deben ser ejecutarse de una forma "todo o nada", eso quiere decir que todas las acciones dentro de la transacción deben ser exitosas o ninguna debe de serlo. De esta forma se asegura la integridad y consistencia de los datos.

El concepto de transacciones puede ser descrito con las siguientes cuatro propiedades conocidas como ACID, por sus siglas en inglés:
  • Atomicity: Una transacción debe ser tratada como una sola operación u unidad de trabajo, lo que significa que toda la secuencia de operaciones debe ser exitosa o ninguna debe de serlo.
  • Consistency: Representa la consistencia o integridad referencial de la base de datos, llaves primarias únicas en trablas, etc.
  • Isolation: Puede haber muchas transacciones procesándose dentro del mismo conjunto de datos al mismo tiempo, cada transacción debe ser aislada de otras para prevenir corrupción de datos.
  • Durability: Una vez que una transacción se ha completado, el resultado de esta transacción debe ser hecha de forma permanente y no debe ser borrada de la base de datos aunque el sistema falle.

El manejo de las transacciones a la base de datos es una de las razones principales para usar Spring. El framework de Springproporciona una capa de abstracción consistente para el manejo de transacciones y agrega los siguientes beneficios:
  • Modelo consistente de programación a través de distintas APIs como Java Transaction API (JTA), JDBCHibernate, Java Persistence API (JPA), y Java Data Objects (JDO).
  • Soporte para manejo declarativo de transacciones.
  • Soporte para manejo programático de transacciones.
  • Excelente integración con la abstracción de datos de Spring (lo que hemos visto en este tutorial).

La idea de manejar transacciones con Spring es que lo haremos de manera similar sin importar cuál mecanismo de transacciones usemos. Además la idea es que sea una alternativa a las transacciones EJB y agregar capacidades transaccionales a los POJOs.

Antes de comenzar con el código debemos entender un concepto que es la propagación de la transacción.

Cuando se trabaja con transacciones manejadas por Spring el desarrollador debe especificar cómo se debe comportar una transacción en términos de propagación, o sea debe decidir cómo deben encapsularse los métodos de negocio en cuanto a las transacciones lógicas y físicas. Los métodos en distintos beans pueden ejecutarse en el scope de la misma transacción o ser divididos en múltiples transacciones. Esto hace que se deba tomar en cuenta detalles como la forma en que la transacción interna afecta la transacción externa.

Existen siete comportamientos de propagación de transacciones:
  • REQUIRED
  • REQUIRES_NEW
  • NESTED
  • MANDATORY
  • NEVER
  • NOT_SUPPORTED
  • SUPPORTS

Propagación REQUIRED

Indica que la misma transacción será usada si ya existe una transacción abierta en el contexto de ejecución del método actual del bean. Si no existe una transacción Spring creará una nueva. Si múltiples métodos configurados como "REQUIRED" son llamados de manera anidada cada uno será asignado a transacciones lógicas distintas pero a la misma transacción física. Esto significa que si un método interno causa el rollback de una transacción, el método externo fallará al realizar un commit y también se le hará un rollback.

Propagación REQUIRES_NEW

Indica que siempre se necesitará una nueva transacción física en el contenedor. En otras palabras, la transacción interna puede hacercommit o rollback independientemente de la transacción externa, o sea que la transacción externa no será afectada por el resultado de la transacción interna ya que se ejecutan en distintas transacciones físicas.

Propagación NESTED

Hace que las distintas transacciones usen la misma transacción física pero establece savepoints entre las distintas invocaciones a métodos de tal forma que las transacciones internas pueden hacer rollback de manera independiente a la transacción externa. Para esto se usan savepoints de JDBC por lo que este comportamiento sólo debería ser usado cuando se usa Spring con transacciones manejadas por JDBC.

Propagación MANDATORY

Indica que ya debe existir una transacción iniciada previamente para que el método pueda ser ejecutado. Si no existe una transacción se lanzará una excepción.

Propagación NEVER

Indica que no debe existir una transacción previamente iniciada. Si una transacción existe se lanzará una excepción.

Propagación NOT_SUPPORTED

Se ejecutará fuera del scope de cualquier transacción. Si ya existe una transacción abierta esta será pausada.

Propagación SUPPORTS

Se ejecutará en el scope de la transacción si ya existe una abierta. Si no hay una transacción el método se ejecutará de todas formas pero de una forma no transaccional.

Dependiendo del tipo de operación que estemos realizando nos convendrá usar un comportamiento de propagación u otro.

Como ya es costumbre Spring proporciona dos formas de indicar el manejo de transacciones, una mediante configuraciones en un archivo XML y otra mediante código. Comenzaremos viendo la forma de declaración de transacciones en un archivo de configuración enXML.


2.1. DECLARACIÓN DE TRANSACCIONES EN ARCHIVOS DE MAPEO XML

Lo primero que debemos hacer es declarar el uso de un manejador de transacciones. Esta debe ser una implementación de la interface "PlatformTransactionManager". Existen varias implementaciones concretas que podemos utilizar dependiendo del mecanismo de persistencia que usemos:
  • "HibernateTransactionManager", para cuando trabajamos con Hibernate 3 o 4.
  • "DataSourceTransactionManager", para cuando trabajamos con un DataSource JDBC.
  • "JpaTransactionManager", para cuando trabajamos con una implementación de JPA
  • "JdoTransactionManager", para cuando trabajamos con JDO.
  • "JtaTransactionManager", para cuando usamos JTA como mecanismo de manejo de transacciones.
  • "JmsTransactionManager", para cuando realizamos transacciones en un servidor JMS.
  • "CciLocalTransactionManager", para cuando debemos sincronizar transacciones con un recurso CCI (Common Client Interface).

Para este ejemplo usaremos el manejador de transacciones para Hibernate 4 que se encuentra en el paquete "org.springframework.orm.hibernate4" (si trabajan con Hibernate 3 deben usar el del paquete correspondiente).

Debemos crear un bean que debe recibir como propiedad el bean de tipo "LocalSessionFactoryBean" que creamos anteriormente con el nombre de "sessionFactory", de esta forma indicamos que este bean debe manejar las transacciones que se realicen con esesessionFactory:

<bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory"/>
</bean>

Spring tiene dos formas de marcar las transacciones, una programática que no veremos en estos tutoriales porque son invasivos en el código (y es lo que estamos tratando de evitar) y otra declarativa que es la manera preferida de trabajar con Spring ya que sólo debemos indicar cómo deben ser manejadas las transacciones de nuestra aplicación y Spring se encargará de que estas indicaciones se cumplan. Si en algún momento queremos que se manejen de forma distinta sólo es necesario cambiar la configuración sin necesidad de tocar nuevamente el código.

El manejo declarativo de transacciones en Spring se hace posible mediante Programación Orientada a Aspectos. Con esto es posible especificar el comportamiento transaccional a nivel de método.

Cuando trabajamos con programación orientada a aspectos en Spring debemos indicarlo mediante el namespace "aop", este contiene elementos que nos ayudarán a marcar los puntos de corte, que son las definiciones de las firmas de los métodos en los que se aplicarán los advices (que en este caso es la indicación del manejo de transacciones).

Para comprender lo siguiente deben entender un poco de Programación Orientada a Aspectos, no es el tema de este tutorial, así que les recomiendo leer el siguiente documento http://computacion.cs.cinvestav.mx/~acaceres/courses/udo/poo/files/slides/POA-01.pdf (la imagen de la presentación es un poco… bueno ya lo verán, pero la información es muy buena ñ_ñ).

En Spring el pointcut designator que usamos con más frecuencia es "execution", el cual se refiere a la ejecución de un método.

Con la teoría anterior espero que sea suficiente para entender lo que haremos a continuación.

Primero usaremos el elemento "config" del namespace "aop" para indicar que configuraremos un pointcut:

<aop:config>
</aop:config>

Dentro de este indicaremos los pointcuts a los que afectaremos, nosotros queremos que sean todos los métodos, sin importar los parámetros que reciban, de todas las clases que se encuentren dentro del paquete "com.javatutoriales.spring.integracion.persistencia", que es donde tenemos las clases para manejo de persistencia. Hasta ahora nuestro pointcut se ve así:

com.javatutoriales.spring.integracion.persistencia.*.*(..)

También debemos indicar que no nos importa el tipo de retorno que tenga el método, para esto usamos el comodín:

* com.javatutoriales.spring.integracion.persistencia.*.*(..)

Finalmente debemos indicar un pointcut designator, para indicar cuándo queremos que se aplique este aspecto cada vez que se ejecute el método indicado, para eso usamos el designador execution. Nuestro pointcut queda de esta forma:

execution(* com.javatutoriales.spring.integracion.persistencia.*.*(..))

Ahora debemos indicar esto dentro del elemento "aop:config". Para ello usamos el elemento "aop:pointcut" al cual debemos, como a todo bean de Spring, darle un identificador y debemos colocar la expresión anterior en al atributo "expression" del elemento:

<aop:config>
    <aop:pointcut id="pointcutsPersistencia" expression="execution(* com.javatutoriales.spring.integracion.persistencia.*.*(..))"/>
</aop:config>

Dejaremos este elemento por el momento para continuar creando el advice, que es el bean que entrará en acción cuando se ejecute elendpoint correspondiente. Para esto usaremos el namespace "tx", con su elemento "advice", a este debemos darle un identificador e indicar el manejador de transacciones que usará. En nuestro caso el identificador que le daremos será "adviceTransacciones" y el manejador de transacciones será "transactionManager", que es el bean de tipo "HibernateTransactionManager" que definimos anteriormente:

<tx:advice id="adviceTransacciones" transaction-manager="transactionManager">
</tx:advice>

Como un tip, si llamamos a nuestro manejador de transacciones "transactionManager" podemos omitir el atributo "transaction-manager" (si tiene cualquier otro nombre deberemos agregarlo) quedando de la siguiente manera:

<tx:advice id="adviceTransacciones">
</tx:advice>

Lo siguiente que debemos hacer es indicar la forma en la que deben comportarse cada uno de los métodos de las clases que hemos indicado. Para eso usamos el elemento "tx:method". Este elemento cuenta con varios atributos que nos permitirá, entre otras cosas, indicar el patrón de nombres de los métodos a los que se aplicará la configuración, el comportamiento de propagación que tendrá la transacción del método, el nivel de aislamiento, el timeout de la transacción, etc.

La siguiente tabla muestra los elementos que podemos configurar y sus valores por default.

AtributoRequeridoValor por DefaultDescripción
nameSi-El nombre del (los) método(s) a los que se asociarán los atributos de la transacción. El caracter comodín (*) puede ser usado para asociar los mismos atributos de la transacción con varios métodos como obten*maneja*,guarda*, etc.
propagationNoREQUIREDEl comportamiento de propagación de la transacción.
isolationNoDEFAULTEl nivel de aislamiento de la transacción.
timeoutNo-El timeout para que la transacción se termine, en segundos.
read-onlyNoFalseIndica si la transacción será de sólo lectura.
rollback-forNo-Excepciones que causarán un rollback de la transacción (lo explicaremos más adelante).
no-rollback-forNo-Excepciones que NO causarán un rollback de la transacción (lo explicaremos más adelante).


El elemento "tx:method" debe estar envuelto en un elemento "tx:attributes", de la siguiente forma:

<tx:advice id="adviceTransacciones">
    <tx:attributes>
    </tx:attributes>
</tx:advice>

Para nuestro ejemplo configuraremos varios métodos. Primero haremos que todos los métodos que inician con "obten", que es la convención que estamos usando para los métodos que obtienen información de la base de datos. Queremos que estos métodos sean de sólo lectura, y no importa si existe una transacción o no en la base de datos, por lo que usaremos el comportamiento de propagaciónSUPPORTS. El resto de los atributos pueden quedarse con sus valores por default.

<tx:method name="obten*" propagation="SUPPORTED" read-only="true" />

Ahora configuraremos el resto de los métodos que son el método para agregar, eliminar y actualizar un Contacto. El comportamiento de estos métodos será prácticamente igual, deben usar la transacción existente o iniciar una nueva si no existe y debe modificar la base de datos. En este caso podemos configurarlos usando un elemento "tx:method" para cada uno, o usando el caracter comodín (esto dependerá de cuántos métodos más tengan sus clases, para el caso de este ejemplo es inofensivo pero deben tener cuidado con esto). La configuración para el resto de los métodos queda de la siguiente forma:

<tx:method name="*" propagation="REQUIRED" />

Y como el valor por default para el atributo "propagation" es "REQUIRED" puede quedar sólo así:

<tx:method name="*" />

Y la configuración completa del advice queda así:

<tx:advice id="adviceTransacciones">
    <tx:attributes>
        <tx:method name="obten*" propagation="SUPPORTS" read-only="true" />
        <tx:method name="*" />
    </tx:attributes>
</tx:advice>

El último paso es regresar a la configuración de nuestro aspecto e indicar qué advice se aplicará a cuál pointcut, para esto usamos el elemento "aop:advisor" dentro del elemento "aop:config". Aquí indicaremos el advice usando el atributo "advice-ref" y el pointcut con el atributo "pointcut-ref", de la siguiente forma:

<aop:advisor advice-ref="adviceTransacciones" pointcut-ref="pointcutsPersistencia"/>

La configuración completa del pointcut queda de la siguiente forma:

<aop:config>
    <aop:pointcut id="pointcutsPersistencia" expression="execution(* com.javatutoriales.spring.integracion.persistencia.*.*(..))"/>
    <aop:advisor advice-ref="adviceTransacciones" pointcut-ref="pointcutsPersistencia"/>
</aop:config>

También debemos hacer un cambio en la clase "ContactosDAOHibernateImpl". Ya que no estaremos manejando las transacciones en esta clase podemos quitar todo el código referente a estas. Eso quiere decir eliminar el atributo de tipo "Transaction" que tiene y los lugares a donde se hace referencia (que son básicamente los lugares donde se hacen "commits" y "rollbacks") y los lugares donde se cierra la sesión y se inicia la transacción. Por lo tanto único que dejaremos es el inicio y uso de la sesión. La clase "ContactosDAOHibernateImpl" queda de la siguiente forma:

public class ContactosDAOHibernateImpl implements ContactosDAO {

    private Session sesion;
    private SessionFactory sessionFactory;

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    @Override
    public long guardaContacto(Contacto contacto) throws HibernateException {
        long id = 0;
        try {
            iniciaOperacion();
            id = (Long) sesion.save(contacto);
        } catch (HibernateException he) {
            throw he;
        }
        return id;
    }

    @Override
    public void actualizaContacto(Contacto contacto) throws HibernateException {
        try {
            iniciaOperacion();
            sesion.update(contacto);
        } catch (HibernateException he) {
            throw he;
        } 
    }

    @Override
    public void eliminaContacto(Contacto contacto) throws HibernateException {
        try {
            iniciaOperacion();
            sesion.delete(contacto);
        } catch (HibernateException he) {
            throw he;
        } 
    }

    @Override
    public Contacto obtenContacto(long idContacto) throws HibernateException {
        Contacto contacto = null;
        iniciaOperacion();
        contacto = (Contacto) sesion.get(Contacto.class, idContacto);

        return contacto;
    }

    @Override
    public List<Contacto> obtenListaContactos() throws HibernateException {
        List<Contacto> listaContactos = null;

        iniciaOperacion();
        listaContactos = sesion.createQuery("from Contacto").list();

        return listaContactos;
    }

    private void iniciaOperacion() throws HibernateException {
        sesion = sessionFactory.getCurrentSession();
    }
}

Como vemos la clase queda mucho más corta y podemos eliminar el método "manejaExcepcion".

Si ahora volvemos a ejecutar nuestra clase "Main" debemos ver la siguiente salida en la consola:

Hibernate: insert into CONTACTOS (NOMBRE, email, telefono) values (?, ?, ?)
Hibernate: select contacto0_.ID as ID1_0_0_, contacto0_.NOMBRE as NOMBRE2_0_0_, contacto0_.email as email3_0_0_, contacto0_.telefono as telefono4_0_0_ from CONTACTOS contacto0_ where contacto0_.ID=?
Nombre: Nombre 1
Email: Email 1

Que como podemos ver es la misma salida de la consola que la vez pasada, con esto comprobamos que la forma declarativa de transacciones funciona correctamente ^_^.

Ahora veremos la última parte de este tutorial en el que veremos cómo hacer en manejo de transacciones por medio de anotaciones.


2.2. DECLARACIÓN DE TRANSACCIONES EN ANOTACIONES

Para trabajar con anotaciones hay que indicarlo por medio de un elemento en el archivo de configuración XML:

<tx:annotation-driven />

Este elemento indicará que las transacciones serán marcadas por medio de anotaciones en las clases que estén contenidas en el paquete indicado por el elemento "<context:component-scan>". En este elemento debemos indicar cuál es el manejador de transacciones que será usado; si el nombre de nuestro manejador de transacciones es "transactionManager" podemos dejar el elemento "<tx:annotation-drive />" vacío, de lo contrario debemos usar el atributo "transaction-manager" para indicar cuál es el nombre del manejador de transacciones. Por ejemplo si el nombre de nuestro bean fuera "txManager" la anotación quedaría:

<tx:annotation-driven transaction-manager="txManager" />

El siguiente cambio necesario es colocar la anotación "@Transactional" en los métodos de la clase "ContactosDAOHibernateImpl". Debemos colocar esta anotación en cada uno de los métodos donde se deba manejar transacciones. Esta anotación puede tener los siguientes atributos, de los cuales ninguno es obligatorio:

AtributoTipoValor por DefaultDescripción
readOnlybooleanfalseIndica si la transacción será de sólo lectura.
timeoutintEl timeout del sistema de transacciones usadoEl timeout para que la transacción se termine, en segundos.
valueString-Un calificador opcional que especifica el manejador de transacciones que se usará (en caso de que usemos varios manejadores de transacciones).
propagationEnum: PropagationPROPAGATION_REQUIREDEl comportamiento de propagación de la transacción.
isolationEnum: IsolationISOLATION_DEFAULTEl nivel de aislamiento de la transacción.
rollbackForArreglo de objetos Classque deben extender deThrowable-Excepciones que causarán unrollback de la transacción (lo explicaremos más adelante).
rollbackForClassNameArreglo de String con el nombre de las clases que deben extender deThrowbale-Nombres de las clases de Excepciones que causarán unrollback.
noRollbackForArreglo de objetos Classque deben extender deThrowable-Excepciones que NO causarán unrollback de la transacción (lo explicaremos más adelante).
noRollbackForClassNameArreglo de String con el nombre de las clases que deben extender deThrowable-Arreglo de nombres que NOcausarán el rollback de la transacción.
Lo que debemos hacer son dos pasos simples. Primero colocar la anotación "@Transactional" en cada uno de los métodos transaccionales de "ContactosDAOHibernateImpl", que son:
  • guardaContacto
  • actualizaContacto
  • eliminaContacto
  • obtenContacto
  • obtenListaContactos

Esta anotación deberíamos de colocarla sólo en los métodos públicos, si la colocamos en otro tipo de métodos no obtendremos ninguna excepción pero tampoco funcionará ^_^!

Debemos indicar también cuál será el comportamiento de nuestras transacciones y si estas serán de sólo lectura.

En nuestro caso los métodos que sólo obtienen información (los que inician con "obten") son métodos de sólo lectura y con un comportamiento de propagación de tipo "SUPPORTS", por lo que la anotación queda de la siguiente forma (recuerden que debemos usar la anotación "@Transactional" del paquete "org.springframework.transaction.annotation"):

@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public Contacto obtenContacto(long idContacto) throws HibernateException

@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public List<Contacto> obtenListaContactos() throws HibernateException

El resto de los métodos usan los valores por default de la anotación, por lo que pueden quedar de la siguiente forma:

@Transactional
public long guardaContacto(Contacto contacto) throws HibernateException

@Transactional
public void actualizaContacto(Contacto contacto) throws HibernateException

@Transactional
public void eliminaContacto(Contacto contacto) throws HibernateException

El segundo paso es, al igual que lo hicimos al trabajar con archivos de mapeo, quitar el atributo de tipo "Transaction", junto con sus usos dentro de la clase, y los lugares donde se hace commit, rollback o se cierra la sesión, ya que Spring se encargará ahora de eso.

La clase "ContactosDAOHibernateImpl" queda de la siguiente forma:

@Repository
public class ContactosDAOHibernateImpl implements ContactosDAO {

    private Session sesion;

    @Autowired
    private SessionFactory sessionFactory;

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    @Override
    @Transactional
    public long guardaContacto(Contacto contacto) throws HibernateException {
        long id = 0;
        try {
            iniciaOperacion();
            id = (Long) sesion.save(contacto);
        } catch (HibernateException he) {
            throw he;
        } 

        return id;
    }

    @Override
    @Transactional
    public void actualizaContacto(Contacto contacto) throws HibernateException {
        try {
            iniciaOperacion();
            sesion.update(contacto);
        } catch (HibernateException he) {
            throw he;
        } 
    }

    @Override
    @Transactional
    public void eliminaContacto(Contacto contacto) throws HibernateException {
        try {
            iniciaOperacion();
            sesion.delete(contacto);
        } catch (HibernateException he) {
            throw he;
        } 
    }

    @Override
    @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
    public Contacto obtenContacto(long idContacto) throws HibernateException {
        Contacto contacto = null;
        iniciaOperacion();
        contacto = (Contacto) sesion.get(Contacto.class, idContacto);

        return contacto;
    }

    @Override
    @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
    public List<Contacto> obtenListaContactos() throws HibernateException {
        List<Contacto> listaContactos = null;

        iniciaOperacion();
        listaContactos = sesion.createQuery("from Contacto").list();

        return listaContactos;
    }

    private void iniciaOperacion() throws HibernateException {
        sesion = sessionFactory.getCurrentSession();
    }
}
Si volvemos a ejecutar nuestra clase "Main" obtendremos la siguiente salida en consola:

Hibernate: insert into contactos (e_mail, nombre, telefono) values (?, ?, ?)
Hibernate: select contacto0_.id as id1_0_0_, contacto0_.e_mail as e_mail2_0_0_, contacto0_.nombre as nombre3_0_0_, contacto0_.telefono as telefono4_0_0_ from contactos contacto0_ where contacto0_.id=?
Nombre: Nombre 1
Email: Email 1

Con lo que comprobamos que la aplicación funciona correctamente.

Para terminar este tutorial veremos cómo manejar las excepciones para hacer rollbacks de forma automática desde Spring.


3. Manejo de Excepciones para Rollback Automático

Una parte importante del trabajo con base de datos es que cuando ocurra algún problema con nuestra aplicación las operaciones dentro de estas se deshagan de forma automática (o sea que ocurra un rollback).

Por default Spring hace un rollback de toda excepción de tipo Runtime y de las clases que extienden de Error. Las excepciones verificadas que sean lanzadas desde un método transaccional no resultarán en un rollback a menos que lo indiquemos de forma explícita en configuración.

Veamos un ejemplo de esto. Modificaremos un poco la clase "ContactosDAOHibernateImpl" para hacer que falle después de guardar un Contacto. Agregaremos la siguiente línea de código para que se lance una "IllegalStateException" (que extiende deRuntimeException) justo después de guardar el Contacto:

if(true)
{
    throw new IllegalStateException("oops");
}

El método queda de la siguiente forma:

public long guardaContacto(Contacto contacto) throws HibernateException {
    long id = 0;
    try {
        iniciaOperacion();
        id = (Long) sesion.save(contacto);
        
        if(true)
        {
            throw new IllegalStateException("oops este contacto no se guardará");
        }
    } catch (HibernateException he) {
        throw he;
    }

    return id;
}

Si limpiamos nuestra base de datos y ejecutamos nuevamente nuestra clase "Main" veremos la siguiente salida:

Hibernate: insert into contactos (e_mail, nombre, telefono) values (?, ?, ?)
Exception in thread "main" java.lang.IllegalStateException: oops este contacto no se guardará

Como vemos la excepción de lanza y si revisamos nuevamente nuestra base de datos veremos que esta continúa vacía. Hasta aquí todo normal.

Ahora veremos que este comportamiento cambia cuando se lanza una excepción verificada. Primero crearemos un nuevo paquete para excepciones: "com.javatutoriales.spring.integration.hibernate.excepciones". Dentro de este paquete crearemos una nueva clase llamada "DatabaseException" que extienda de "Exception":

public class DatabaseException extends Exception {
}

Esta clase tendrá un constructor que reciba una cadena que será el mensaje que se mostrará en la consola cuando se muestre el stacke trace de la excepción. La clase "DatabaseException" completa queda de la siguiente forma:

public class DatabaseException extends Exception {

    public DatabaseException(String string) {
        super(string);
    }
}

Ahora modificaremos la interface "ContactosDAO" para hacer que el método "guardaContacto" lance una "DatabaseException" en vez de la "HibernateException" que lanza actualmente:

long guardaContacto(Contacto contacto) throws DatabaseException;

Con este cambio también será necesario cambiar la firma del método en la clase "ContactosDAOHibernateImpl" para que también lance la "DatabaseException"; modificaremos el cuerpo del método para lanzar una "DatabaseException" en vez de la "IllegalStateException":

public long guardaContacto(Contacto contacto) throws DatabaseException {
    long id = 0;
    try {
        iniciaOperacion();
        id = (Long) sesion.save(contacto);
        if(true)
        {
            throw new DatabaseException("Este contacto se guardará");
        }
    } catch (HibernateException he) {
            throw he;
    }

    return id;
}

Si volvemos a ejecutar nuestra aplicación veremos la siguiente salida en la consola:

Hibernate: insert into contactos (e_mail, nombre, telefono) values (?, ?, ?)
Exception in thread "main" com.javatutoriales.spring.integration.hibernate.excepciones.DatabaseException: Este contacto se guardará

Como vemos se vuelve a lanzar la excepción pero si revisamos la base de datos veremos que en este caso el Contacto si fue almacenado. Con esto podemos comprobar que, efectivamente, el rollback ocurre cuando se lanza una excepción de tipo Runtime.

Spring nos permite definir cuáles excepciones causarán que ocurra un rollback y cuáles no, con esto podríamos definir que cuando se lance una "DatabaseException" ocurra un rollback y cuando se lance una supuesta "DataNotFoundException" no ocurra elrollback. O de forma similar, podríamos definir que cuando ocurra una "IllegalStateException" no curra un rollback pero sí cuando ocurra una "IllegalArgumentException".

Hacer esto es sumamente simple y depende de cómo estemos trabajando. Si hacemos uso de archivos de configuración en XMLdebemos usar el atributo "rollback-for" del elemento "<tx:method>" en el archivo de configuración de Spring para indicar las excepciones que causarán un rollback y "no-rollback-for" para las excepciones que NO causarán un rollback. El nombre de la clase puede ser el fully qualified class name o el nombre simple siempre y cuando no haya un conflicto con el nombre de otra clase. Si vamos a especificar más de una clase de cualquiera de los dos atributos debemos separarlos por coma.

Por ejemplo, supongamos que queremos indicar que se debe hacer rollback para "DatabaseException" y que no se debe hacer para "IllegalStateException" y "IllegalArgumentException", la configuración del advice quedaría de la siguiente forma:

<tx:advice id="adviceTransacciones">
    <tx:attributes>
        <tx:method name="obten*" propagation="SUPPORTS" read-only="true" rollback-for="DatabaseException" no-rollback-for="IllegalStateException, IllegalArgumentException" />
        <tx:method name="*" rollback-for="DatabaseException" no-rollback-for="IllegalStateException, IllegalArgumentException" />
    </tx:attributes>
</tx:advice>

Para el caso de las anotaciones hay que usar el atributo "rollbackFor" para las excepciones que deben causar un rollback y "noRollbackFor" para las excepciones que no deben causar un rollback. En ambos casos se debe usar un arreglo de objetos class (existe otra versión que usa cadenas con el nombre de la clase, pero para este ejemplo es más fácil usar estos atributos). Si vamos a indicar sólo una clase podemos usar un objeto Class simple; si vamos a indicar varias clases debemos indicar un arreglo de objetosClass, los cuales marcamos entre corchetes. La configuración para el método "guardaContacto" haciendo uso de anotaciones quedaría de la siguiente forma:

@Transactional(rollbackFor = DatabaseException.class, noRollbackFor = {IllegalStateException.class, IllegalArgumentException.class})

Al ejecutar nuevamente la aplicación debemos ver que el Contacto ya no se almacena en la base de datos gracias a que se hace unrollback de la operación.

Podemos combinar ambos métodos cuando desarrollamos una aplicación para, por ejemplo, indicar mediante el archivo de configuración el comportamiento que deben tener todas las clases en la capa de persistencia y posteriormente anotar en la capa de lógica de la aplicación los métodos que deban ejecutarse de forma atómica, indicando que si ocurre una excepción en cualquiera de los distintos métodos de la capa de persistencia que sean llamados debe hacerse un rollback de todas las operaciones dejando consistente toda nuestra información.

Como podemos ver, integrar Hibernate a nuestras aplicaciones hechas con Spring es simple, sólo hay que saber qué configurar y tener claro el comportamiento que deben tener nuestras transacciones, las cuales ya hemos visto que también podemos indicar de varias formas. Recuerden que también podemos trabajar mezclando anotaciones y la configuración en el archivo de Spring.

Espero que este tutorial les sea de utilidad. Si tienen alguna duda, sugerencia, comentario o aclaración, pueden dejarla en la sección de comentarios o enviar un correo a programadorjavablog@gmail.com (pueden agregarme al chat de gmail). También pueden seguirJavaTutoriales en las siguientes redes sociales:


Saludos y gracias.

No hay comentarios:

Publicar un comentario