lunes, 23 de agosto de 2010

Hibernate - Parte 11: Interceptores y Eventos

Algunas veces podemos tener situaciones que demanden la realización de algunas operaciones antes o después de nuestra lógica funcional (precondiciones y postcondiciones). O tal vez simplemente queremos intervenir antes o después de qué alguna de nuestras operaciones de persistencia (alta, baja, actualización, lectura, etc.) sea realizada. Ya sea para, por ejemplo, modificar algún valor de nuestra entidad (como encriptar algún dato antes de ser guardado) o para leer algún valor.

También algunas veces es necesario recibir alguna notificación de algún suceso que esté ocurriendo en nuestro motor de persistencia, como el estar recuperando o eliminando algún objeto. Esto puede ser útil para propósitos de auditorías, o para obtener estadísticas sobre las operaciones de persistencia en nuestras aplicaciones.

Hibernate proporciona dos mecanismos para lograr estos dos objetivos: listeners y eventos.

En este tutorial aprenderemos cómo recibir notificaciones cada vez que Hibernate realice alguna operación de persistencia a través de estos dos mecanismos.


1. Interceptores


Los interceptores nos proporcionan llamadas, a nivel sesión o a nivel aplicación, permitiendo a la aplicación inspeccionar y/o manipular propiedades de un objeto persistente antes de ser guardado, actualizado, eliminado, o cargado dentro de nuestro contexto persistente.

Pueden ser utilizados para monitorear los eventos ocurridos o para sobreescribir la funcionalidad de un módulo. El ejemplo clásico es la auditoría del sistema, para realizar un log de eventos que indiquen los cambios que realizan sobre nuestras entidades.

En nuestro ejemplo realizaremos un mini-sistema de auditoría, en el que queremos que, cada vez que un Usuario sea almacenado o eliminado del sistema, se muestre un mensaje en la consola. Por lo tanto comenzaremos nuestro ejemplo creando un nuevo proyecto.

Creamos un proyecto en NetBeans (menú "File -> New Project... -> Java -> Java Application"). Le damos un nombre y una ubicación al proyecto y nos aseguramos de que las opciones "Create Main Class" y "Set as Main Project" estén habilitadas. Presionamos el botón "Finish" y veremos aparecer en el editor nuestra clase "Main".

Agregamos la biblioteca de "Hibernate", que creamos en el primer tutorial de la serie. Hacemos clic derecho en el nodo "Libraries" del proyecto. En el menú contextual que se abre seleccionamos la opción "Add Library...":



En la ventana que se abre seleccionamos la biblioteca "Hibernate":



Presionamos el botón "Add Library" para que la biblioteca se agregue a nuestro proyecto. Como vamos a usar anotaciones, además debemos agregar la biblioteca "HibernateAnotaciones" que creamos en el segundo tutorial. Aprovechamos también para agregar el conector de MySQL. Debemos tener los siguientes archivos en nuestro proyecto:



Ahora creamos tres paquetes, uno con el nombre "modelo", que contendrá las clases entidades, otro con el nombre "dao" que contendrá nuestras clases para operaciones de persistencia, y el último con el nombre "interceptores" que contendrá el interceptor que crearemos para nuestro ejemplo. Hacemos clic derecho en el nodo del paquete que se creó al generar el proyecto. En el menú contextual que se abre seleccionamos la opción "New -> Java Package..." y creamos los paquetes.



Ahora creamos el archivo de mapeo "hibernate.cfg.xml" en el paquete raíz de nuestra aplicación. Este archivo será muy parecido al del primer tutorial:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>

        <!-- parametros para la conexion a la base de datos -->
        <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
        <property name="connection.url">jdbc:mysql://localhost/hibernateinterceptores</property>
        <property name="connection.username">usuario</property>
        <property name="connection.password">password</property>

        <!-- Configuracion del pool interno -->
        <property name="connection.pool_size">1</property>

        <!-- 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">create-drop</property>

         <!-- Clases o Archivos de mapeo -->
        
    </session-factory>
</hibernate-configuration>


Aprovechamos para agregar, en el paquete "dao" la clase "HibernateUtil" que creamos en el segundo tutorial. Y la clase "AbstractDAO" que creamos en el tutorial 9 (parámetros).

Para terminar crearemos una sola clase entidad llamada "Usuario", en el paquete "modelo", que tendrá solo un par de atributos además del identificador y será anotada con las mismas anotaciones que vimos en el segundo tutorial. La clase queda de la siguiente forma:


@Entity
public class Usuario implements Serializable
{
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long id;
    private String nombre;
    private String username;
    private String password;

    public Usuario()
    {
    }

    public Usuario(String nombre, String username, String password)
    {
        this.nombre = nombre;
        this.username = username;
        this.password = password;
    }

    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 getPassword()
    {
        return password;
    }

    public void setPassword(String password)
    {
        this.password = password;
    }

    public String getUsername()
    {
        return username;
    }

    public void setUsername(String username)
    {
        this.username = username;
    }
}


No olviden agregar esta clase en el archivo de configuración "hibernate.cfg.xml".


<mapping class="hibernate.interceptores.modelo.Usuario" />


Para este ejercicio usaremos una base de datos llamada "hibernateinterceptores".

Ahora comencemos a ver cómo implementar nuestro interceptor.

La forma más "directa" de crear un interceptor es implementar la interface "org.hibernate.Interceptor" la cual tiene nada más y nada menos que 18 métodos. La siguiente tabla muestra una breve descripción de los 13 métodos más importantes de esta interface:

  • afterTransactionBegin – Este método es llamado inmediatamente después de que una transacción es iniciada.
  • afterTransactionCompletion – Llamado al terminar una transacción.
  • beforeTransactionCompletion – Llamado antes de que se realice un commit de la transacción (solo de commit, no de rollback).
  • findDirty – Llamado en el momento de llamar a "flush()".
  • onCollectionRecreate – Llamado antes de que una colección se creada o recreada.
  • onCollectionRemove – Llamado antes de que una colección sea eliminada.
  • onCollectionUpdate – Llamado antes de que una colección sea actualizada.
  • onDelete – Llamado antes de que una instancia sea eliminada
  • onLoad – Llamado antes de que una entidad sea inicializada (o sea antes de establecer los valores de sus atributos).
  • onPrepareStatement – Llamado cuando se está preparando la cadena con el SQL generado.
  • onSave – Llamado antes de que una entidad sea almacenada.
  • postFlush – Llamado después del flush que sincroniza los datos con la base de datos.
  • preFlush – Llamado antes de un flush.

Como podemos ver, esta interface tiene métodos para prácticamente cualquier cosa que se nos pueda ofrecer (aparentemente). Sin embargo, como sabemos, si queremos implementar una interface entonces debemos proporcionar la implementación de cada uno de los métodos que tiene la interface (y con 18 métodos esto no es una tarea nada fácil). Afortunadamente los creadores de Hibernate nos ofrecen una manera de no tener que implementar, por nosotros mismos, cada uno de estos métodos, a través de la clase "org.hibernate.EmptyInterceptor", la cual funciona como adapter de la interface "org.hibernate.Interceptor". Esta clase contiene implementaciones vacías de los 18 métodos de la interface "org.hibernate.Interceptor", por lo que solo deberemos sobreescribir los métodos que nos interesen.

Será a través de esta última clase que realizaremos nuestro ejemplo.

Creamos, en nuestro paquete "interceptores", una clase llamada "InterceptorAuditoria". Que se encargará de realizar los procesos de auditoría de los que hablamos. Esta clase debe extender de "EmptyInterceptor", de la siguiente forma:


public class InterceptorAuditoria extends EmptyInterceptor
{
}


Repasemos los dos requerimientos que nuestra clase resolverá:

  • 1. Cada vez que un Usuario sea almacenado se debe mostrar un mensaje en consola.
  • 2. Cada vez que un Usuario sea eliminado se debe mostrar un mensaje en consola.

Si revisamos la lista de métodos de la interface "Interceptor" vemos que tiene 2 que podrían servirnos: "onSave" para el requerimiento número 1, y "onDelete" para el requerimiento número 2. Por lo que sobreescribimos estos dos método en la clase "InterceptorAuditoria".


public class InterceptorAuditoria extends EmptyInterceptor
{
    @Override
    public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types)
    {
    }

    @Override
    public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types)
    {
    }
}


Veamos primero cómo trabaja el método "onSave" y cómo lo usaremos para nuestros propósitos de auditoría.

boolean onSave(Object entity,
               Serializable id,
               Object[] state,
               String[] propertyNames,
               Type[] types)
               throws CallbackException


Como dice en la lista: "onSave" es llamado antes de que un objeto sea guardado. Lo primero que podemos ver en la firma del método nos indica que este regresa un booleano. Este valor indica si nosotros hemos modificado este objeto de alguna forma, como por ejemplo modificando alguno de los valores del mismo. Como para nuestros propósitos esto no es necesario regresaremos "false":


@Override
public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types)
{
    return false;
}


Dentro de la lista de parámetros que recibe este método podemos ver que solamente nos interesa uno, de hecho el primero: "entity" que es la entidad que será almacenada en nuestra base de datos. Para poder saber si estamos guardando un "Usuario" o algún otro tipo de entidad debemos usar el operador "instanceof". Después haremos un cast al tipo "Usuario" para poder obtener de este su "nombre" y su "username" de la siguiente forma:

if(entity instanceof Usuario)
{
    Usuario usuario = (Usuario)entity;
    System.out.println("Se ha almacenado al Usuario " + usuario.getNombre() + ", \"" + usuario.getUsername() + "\"");
}


Y listo, con este pequeño fragmento de código hemos cumplido con el primer requerimiento de nuestro sistema de auditoría. Nuestro método "onSave" queda de la siguiente forma:


public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types)
{
    if(entity instanceof Usuario)
    {
        Usuario usuario = (Usuario)entity;
        System.out.println("Se ha almacenado al Usuario " + usuario.getNombre() + ", \"" + usuario.getUsername() + "\"");
    }
        
    return false;
}


Ahora veamos nuestro segundo requerimiento: "Cada vez que un Usuario sea eliminado se debe mostrar un mensaje en consola". Para el cual dijimos que usaremos el método "onDelete":

void onDelete(Object entity,
              Serializable id,
              Object[] state,
              String[] propertyNames,
              Type[] types)
              throws CallbackException


Recordemos que, según la lista, "onDelete" es llamado antes de que la entidad sea eliminada. Podemos ver en la firma del método que, a diferencia de "onSave", "onDelete" no regresa nada. Esto es debido a que como la entidad va a ser eliminada, no tiene sentido modificar los atributos del objeto.

Dentro de los parámetros que recibe el método, ahora hay 2 que nos interesan: "entity", que es la entidad que se va a eliminar, e "id", que es el identificador de dicha entidad en la base de datos.

Nuevamente, dentro del método "onDelete" usaremos el operador "instanceof" para determinar si estamos eliminando un "Usuario". Después haremos un cast al tipo "Usuario" para poder obtener de este su "nombre" y usaremos el argumento "id" para mostrar el identificador de esta entidad (el cual también podríamos obtener invocando al método "getId()" de nuestro Usuario):

if(entity instanceof Usuario)
{
    Usuario usuario = (Usuario)entity;
    System.out.println("Se eliminará al Usuario " + usuario.getNombre() + ", id=" + id);
}


Y listo, esto es suficiente para cumplir con nuestro segundo requerimiento. El método "onDelete" queda de la siguiente forma:

public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types)
{
    if(entity instanceof Usuario)
    {
        Usuario usuario = (Usuario)entity;
        System.out.println("Se eliminará al Usuario " + usuario.getNombre() + ", id=" + id);
    }
}


Y nuestra clase "InterceptorAuditoria" así:

public class InterceptorAuditoria extends EmptyInterceptor
{
    @Override
    public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types)
    {
        if(entity instanceof Usuario)
        {
            Usuario usuario = (Usuario)entity;
            System.out.println("Se ha almacenado al Usuario " + usuario.getNombre() + ", \"" + usuario.getUsername() + "\"");
        }
        
        return false;
    }

    @Override
    public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types)
    {
        if(entity instanceof Usuario)
        {
            Usuario usuario = (Usuario)entity;
            System.out.println("Se eliminará al Usuario " + usuario.getNombre() + ", id=" + id);
        }
    }
}


Ya teniendo listo nuestro interceptor, lo siguiente que debemos hacer es indicarle a Hibernate que este interceptor existe para que pueda utilizarlo. Este es el momento indicado para decir que Hibernate maneja dos tipos de Interceptores:

  • Interceptores de Session (Session-scoped)
  • Interceptores de SessionFactory (SessionFactory-scoped)

Los interceptores de session son especificados cuando se abre una sesión, usando el método "openSession" de "SessionFactory" (que en nuestra clase "AbstracDAO" ocurre en el método "iniciaOperacion"), y funcionan únicamente para la sesión que se está abriendo.

Los interceptores de SessionFactory se registran con el objeto "org.hibernate.cfg.Configuration" que usamos para construir la "SessionFactory", usando el método "buildSessionFactory" (que en nuestra clase "HibernateUtil" ocurre en el constructor estático). Estos interceptores son usados en todas las sesiones que se abran en nuestra aplicación (a menos que especifiquemos interceptores para una sesión específica usando los interceptores de session).

Como en este caso nuestro interceptor solo el útil si estamos trabajando con entidades "Usuario", usaremos interceptores de session. Por lo que modificaremos un poco nuestra clase "AbstracDAO" para que pueda recibir un interceptor cada vez que vayamos a almacenar una entidad.

Lo primero que haremos es agregar un nuevo método "iniciaOperacion" que reciba como parámetro nuestro interceptor. He optado por hacer esto ya que es la opción que requiere menos cambios del código que ya tenemos. Así que nuestro nuevo método "iniciaOperacion" queda de la siguiente forma:

protected void iniciaOperacion(Interceptor interceptor)
{
    sesion = HibernateUtil.getSessionFactory().openSession(interceptor);
    sesion.getTransaction().begin();
}


Este nuevo método "iniciaOperacion" lo usaremos en el método "almacenaEntidad" de la misma clase "AbstractDAO", de la siguiente forma:

dummy.iniciaOperacion(new InterceptorAuditoria());


Esto nos resuelve parte referente al primer requerimiento, pero si revisan la clase "AbstractDAO" verán que no existe ningún método para eliminar entidades, por lo que crearemos uno muy similar a "almacenaEntidad", pero que haga uso del método "delete" en vez de "saveOrUpdate". Este método se llamará "eliminaEntidad", y queda de esta forma:

public static void eliminaEntidad(Object entidad) throws HibernateException
{
    AbstractDAO dummy = new AbstractDAO(){};

    try
    {
        dummy.iniciaOperacion(new InterceptorAuditoria());
        dummy.getSession().delete(entidad);
        dummy.getSession().flush();
    }
    catch(HibernateException he)
    {
        dummy.manejaExcepcion(he);
    }
    finally
    {
        dummy.terminaOperacion();
    }
}


Ahora que ya tenemos los dos métodos listos para usarlos, solo nos falta usarlos en nuestra clase "Main" en nuestro método "main". Crearemos 5 Usuarios y después eliminaremos a 2 de ellos, para ver que, efectivamente, tengamos la salida esperada en la consola. Coloquemos el siguiente código en nuestro método "main":

public static void main(String[] args)
{
    Usuario usuario1 = new Usuario("usuario 1", "username usuario 1", "password usuario 1");
    Usuario usuario2 = new Usuario("usuario 2", "username usuario 2", "password usuario 2");
    Usuario usuario3 = new Usuario("usuario 3", "username usuario 3", "password usuario 3");
    Usuario usuario4 = new Usuario("usuario 4", "username usuario 4", "password usuario 4");
    Usuario usuario5 = new Usuario("usuario 5", "username usuario 5", "password usuario 5");

    AbstractDAO.almacenaEntidad(usuario1);
    AbstractDAO.almacenaEntidad(usuario2);
    AbstractDAO.almacenaEntidad(usuario3);
    AbstractDAO.almacenaEntidad(usuario4);
    AbstractDAO.almacenaEntidad(usuario5);

    AbstractDAO.eliminaEntidad(usuario2);
    AbstractDAO.eliminaEntidad(usuario5);
}


El código es muy simple: creamos 5 Usuarios que posteriormente guardamos en nuestra base de datos usando el método "almacenaEntidad" de la clase "UsuarioDAO", y posteriormente eliminamos a los Usuarios 2 y 5 usando el método "eliminaEntidad" de la clase "UsuarioDAO".

Al ejecutar el código anterior genera la siguiente salida en nuestra consola:



Como podemos ver hemos obtenido los 7 mensajes esperados en consola. Por lo que nuestro ejemplo ha funcionado correctamente ^_^.

Ahora veremos cómo funciona y cómo trabajar con el sistema de eventos de Hibernate.


2. Eventos


Si queremos que nuestra aplicación reaccione a ciertos eventos de nuestra capa de persistencia, podemos usar la arquitectura de eventos de Hibernate. El sistema de eventos puede ser usado para complementar, o reemplazar, el uso de interceptores.

Todos los métodos de la interface "Session" están relacionados con un evento del cual podemos recibir una notificación. Cuando invocamos algún método como "save", "load", "delete", etc. Hibernate lanza un evento y podemos realizar alguna acción en ese momento. Por cada tipo de evento existe una interface "xxEventListener" que tendremos que implementar para recibir notificaciones para ese evento. Estas interfaces reciben un parámetro del tipo del evento que la lanzo. Por ejemplo, si esperamos una notificación de un evento "saveOrUpdate", tendremos que implementar una interface "org.hibernate.event.SaveOrUpdateEventListener" en cuyo método de escucha recibe un objeto de tipo "org.hibernate.event.SaveOrUpdateEvent".

El paquete "org.hibernate.event" contiene las siguientes interfaces que nos permiten recibir notificaciones de eventos:


Para este ejemplo crearemos un sistema que reciba notificaciones después de que una entidad "Usuario" sea cargada de la base de datos, antes y después de eliminarla y antes de actualizarla, y muestre en consola el mensaje correspondiente al eventos que se ha recibido.

Crearemos un nuevo proyecto en NetBeans siguiendo los mismos pasos que usamos para el proyecto que usa los interceptores. Las únicas diferencias son que ahora crearemos un paquete llamados "eventos" en lugar de "interceptores" y que usaremos una base de datos llamada "hibernateeventos".

Usaremos la misma clase "Usuario" del ejemplo anterior, así como las clases "HibernateUtil" del segundo tutorial, y "AbstractDAO" del noveno tutorial.

Hagamos nuevamente una lista con los requerimientos de nuestra aplicación, en lo que a eventos se refiere.

  • 1. Antes de que un Usuario sea cargado debemos mostrar un mensaje en consola.
  • 2. Antes de eliminar un Usuario debemos mostrar un mensaje en consola.
  • 3. Después de eliminar un Usuario debemos mostrar un mensaje en consola.
  • 4. Antes de actualizar un Usuario debemos mostrar un mensaje en consola.

Si echamos un vistazo a la lista listeners de eventos que nos proporciona Hibernate podemos ver que tenemos 4 interfaces que nos sirven perfectamente para cada uno de los requerimientos (vaya coincidencia ^_^!).

Comencemos viendo cómo cubrir el primer requerimiento. Si revisamos la lista de listeners disponibles vemos que hay uno llamado "PostLoadEventListener" el cual, como su nombre lo indica, es llamado después de que la entidad es cargada en el contexto persistente de nuestra aplicación. Como la entidad ya se encuentra cargada, es posible acceder a sus atributos para consultarlos o modificarlos.

Creamos una nueva clase, en el paquete "eventos", llamada "CargaUsuarioListener", la cual implementará la interface "PostLoadEventListener":

public class CargaUsuarioListener implements PostLoadEventListener
{
}
La interface "PostLoadEventListener" tiene un solo método llamado "onPostLoad", el cual recibe un solo argumento de tipo "org.hibernate.event.PostLoadEvent", de la siguiente forma:

public void onPostLoad(PostLoadEvent postLoadEvent);


La clase "PostLoadEvent" tiene dos métodos que nos serán de mucha utilidad: "getEntity()", que regresa un "Object" que es la entidad que se está recuperando, y "getId()", y regesa el identificador de dicha entidad. Primero usaremos "getEntity()" para recuperar la entidad cargada, de la siguiente forma:

Object entidad = postLoadEvent.getEntity();


Después revisamos, usando el operador "instanceof", si la entidad es de tipo "Usuario". De ser el caso, hacemos un cast al tipo "Usuario" para poder obtener su "username". Una vez hecho esto, usamos el método "getId()" para obtener el identificador de la entidad. De la siguiente forma:

if (entidad instanceof Usuario)
{
    Usuario usuario = (Usuario) entidad;

    System.out.println("Se ha cargado el usuario " + usuario.getUsername() + ", id=\"" + postLoadEvent.getId() + "\"");
}


Finalmente la clase "CargaUsuarioListener" queda de la siguiente forma:

public class CargaUsuarioListener implements PostLoadEventListener
{
    public void onPostLoad(PostLoadEvent postLoadEvent)
    {
        Object entidad = postLoadEvent.getEntity();

        if (entidad instanceof Usuario)
        {
            Usuario usuario = (Usuario) entidad;

            System.out.println("Se ha cargado el usuario " + usuario.getUsername() + ", id=\"" + postLoadEvent.getId() + "\"");
        }
    }
}


Veamos ahora el segundo requerimiento: "Antes de eliminar un Usuario debemos mostrar un mensaje en consola". Nuevamente si revisamos la lista de listeners vemos que existe una interface llamada "PreDeleteEventListener" que, como su nombre indica, es llamado antes de que una entidad sea eliminada de la base de datos.

Creamos una nueva clase llamada "PreEliminaUsuarioListener" que implementará la interface "PreDeleteEventListener". De la siguiente forma:

public class PreEliminaUsuarioListener implements PreDeleteEventListener
{
}


La interface "PreDeleteEventListener" tiene un solo método llamado "onPreDelete", que regresa un booleano que indica la operación de eliminación debe ser cancelada. También recibe un único argumento de tipo "org.hibernate.event.PreDeleteEvent". La firma del método es la siguiente:

public boolean onPreDelete(PreDeleteEvent preDeleteEvent);


Lo primero que haremos es regresar un valor de "false", ya que NO queremos que la operación de eliminación sea cancelada.

public boolean onPreDelete(PreDeleteEvent preDeleteEvent)
{
    return false;
}


La clase "PreDeleteEvent" tiene prácticamente los mismos métodos que "PostLoadEvent" (al menos los que nos interesan). Por lo nuevamente usaremos el método "getEntity()" para obtener la entidad que estamos a punto de eliminar, y después revisamos, usando el operador "instanceof", si la entidad es de tipo "Usuario". De ser el caso, hacemos un cast al tipo "Usuario" para poder obtener su "username". Una vez hecho esto, usamos el método "getId()" para obtener el identificador de la entidad. Por lo que el método queda de la siguiente forma:

public boolean onPreDelete(PreDeleteEvent preDeleteEvent)
{
    Object entidad = preDeleteEvent.getEntity();

    if (entidad instanceof Usuario)
    {
        Usuario usuario = (Usuario) entidad;

        System.out.println("Se eliminará al usuario " + usuario.getUsername() + ", id=\"" + preDeleteEvent.getId() + "\"");
    }

    return false;
}


Prosigamos con el tercer requerimiento: "Después de eliminar un Usuario debemos mostrar un mensaje en consola". Para este requerimiento nuevamente haremos uso de una de las interfaces de la lista: "PostDeleteEventListener", así que crearemos una nueva clase llamada "PostEliminaUsuarioListener", en el paquete "eventos", que implemente esta interface, de la siguiente forma:

public class PostEliminaUsuarioListener implements PostDeleteEventListener
{
}


Esta interface tiene un solo método: "onPostDelete", que recibe un parámetro de tipo "org.hibernate.event.PostDeleteEvent", de la siguiente forma:

public void onPostDelete(PostDeleteEvent pde);


Al igual que en los casos anteriores, "PostDeleteEvent" tiene dos métodos que nos interesan "getEntity()", y "getId()". Como creo que la idea de cómo implementaremos nuestros eventos ya está entendida, solo colocaré el código de la clase "PostEliminaUsuarioListener" que se encarga del requerimiento 3 y "ActualizaUsuarioListener" que se encarga del requerimiento 4.

La clase "PostEliminaUsuarioListener" queda de la siguiente forma:

public class PostEliminaUsuarioListener implements PostDeleteEventListener
{
    public void onPostDelete(PostDeleteEvent postDeleteEvent)
    {
        Object entidad = postDeleteEvent.getEntity();

        if (entidad instanceof Usuario)
        {
            Usuario usuario = (Usuario) entidad;

            System.out.println("Se ha eliminado al Usuario " + usuario.getUsername() + ", id=\"" + postDeleteEvent.getId() + "\"");
        }
    }
}


Y la clase "ActualizaUsuarioListener" queda así:

public class ActualizaUsuarioListener implements PreUpdateEventListener
{
    public boolean onPreUpdate(PreUpdateEvent preUploadEvent)
    {
        Object entidad = preUploadEvent.getEntity();

        if(entidad instanceof Usuario)
        {
            Usuario usuario = (Usuario)entidad;

            System.out.println("Se va a actualizar al usuario " + usuario.getUsername() + ", id=\"" + preUploadEvent.getId() + "\"");
        }

        return false;
    }
}


Ahora debemos decirle a Hibernate que queremos que estas clases reciban notificaciones para los eventos indicados. Para esto existen dos formas: la primera y que me parece más sencilla es colocando los listeners en el archivo de configuración "hibernate.cfg.xml" en la configuración del "session-factory", y la segunda es en código, al momento de crear el objeto "org.hibernate.cfg.Configuration" o "org.hibernate.cfg.AnnotationConfiguration" (que nosotros hacemos en la clase "HibernateUtil" en nuestro bloque de inicialización estático). Veremos cómo hacerlo de las dos formas, aunque en los archivos del tutorial dejaré la versión que hace uso del archivo de configuración.


2.1. Configuración de Listeners de Eventos en el Archivo de Configuración


En la primera de las formas debemos colocar, al final del archivo "hibernate.cfg.xml", un elemento "<event>", en cuyo atributo "type" indicamos el tipo de evento que está esperando recibir el listener que indicaremos posteriormente en el sub-elemento "<listener>", en cuyo atributo "class" indicaremos la clase que responderá al evento. Por ejemplo, para indicar que la clase "hibernate.eventos.eventos.CargaUsuarioListener" debe responder al evento de que una entidad haya sido cargada lo hacemos de la siguiente forma:

<event type="post-load">
    <listener class="hibernate.eventos.eventos.CargaUsuarioListener" />
</event>


Los tipos de eventos, como deben ser marcados en el atributo "type" del elemento "<event>" son:

*Nota: Algunos de los listenes pueden tomar más de un valor, en la tabla los he separado usando una coma, pero en su archivo de configuración solo deben colocar uno de los dos.

Clase ListenerValor de typo
AutoFlushEventListenerauto-flush
DeleteEventListenerdelete
DirtyCheckEventListenerdirty-check
EvictEventListenerevict
FlushEntityEventListenerflush-entity
FlushEventListenerFlush
InitializeCollectionEventListenerload-collection
LoadEventListenerload
LockEventListenerLock
MergeEventListenermerge
PersistEventListenercreate, create-onflush
PostCollectionRecreateEventListenerpost-collection-recreate
PostCollectionRemoveEventListenerpost-collection-remove
PostCollectionUpdateEventListenerpost-collection-update
PostDeleteEventListenerpost-delete, post-commit-delete
PostInsertEventListenerpost-insert, post-commit-insert
PostLoadEventListenerpost-load
PostUpdateEventListenerpost-update, post-commit-update
PreCollectionRecreateEventListenerpre-collection-recreate
PreCollectionRemoveEventListenerpre-collection-remove
PreCollectionUpdateEventListenerpre-collection-update
PreDeleteEventListenerpre-delete
PreInsertEventListenerpre-insert
PreLoadEventListenerpre-load
PreUpdateEventListenerpre-update
RefreshEventListenerrefresh
ReplicateEventListenerreplicate
SaveOrUpdateEventListenersave-update, save, update


Por lo tanto, los listeners declarados es nuestro archivo de configuración quedan de la siguiente forma:

<event type="post-load">
    <listener class="hibernate.eventos.eventos.CargaUsuarioListener" />
</event>

<event type="pre-delete">
    <listener class="hibernate.eventos.eventos.PreEliminaUsuarioListener" />
</event>

<event type="post-delete">
    <listener class="hibernate.eventos.eventos.PostEliminaUsuarioListener" />
</event>

<event type="pre-update">
    <listener class="hibernate.eventos.eventos.ActualizaUsuarioListener" />
</event>


Y el archivo completo de configuración quedaría de esta forma:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>

        <!-- parametros para la conexion a la base de datos -->
        <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
        <property name="connection.url">jdbc:mysql://localhost/hibernateeventos</property>
        <property name="connection.username">usuario</property>
        <property name="connection.password">password</property>

        <!-- Configuracion del pool interno -->
        <property name="connection.pool_size">1</property>

        <!-- 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">create-drop</property>

         <!-- Clases o Archivos de mapeo -->
        <mapping class="hibernate.eventos.modelo.Usuario" />

        <event type="post-load">
            <listener class="hibernate.eventos.eventos.CargaUsuarioListener" />
        </event>

        <event type="pre-delete">
            <listener class="hibernate.eventos.eventos.PreEliminaUsuarioListener" />
        </event>

        <event type="post-delete">
            <listener class="hibernate.eventos.eventos.PostEliminaUsuarioListener" />
        </event>

        <event type="pre-update">
            <listener class="hibernate.eventos.eventos.ActualizaUsuarioListener" />
        </event>

    </session-factory>
</hibernate-configuration>


Veamos que nuestra aplicación funciona correctamente colocando el siguiente código en el método "main" de nuestra clase "Main":

public static void main(String[] args)
{
    Usuario usuario1 = new Usuario("usuario 1", "username usuario1", "password usuario1");
    Usuario usuario2 = new Usuario("usuario 2", "username usuario2", "password usuario2");
    Usuario usuario3 = new Usuario("usuario 3", "username usuario3", "password usuario3");
    Usuario usuario4 = new Usuario("usuario 4", "username usuario4", "password usuario4");

//        Almacenamos las 4 entidades Usuario
    AbstractDAO.almacenaEntidad(usuario1);
    AbstractDAO.almacenaEntidad(usuario2);
    AbstractDAO.almacenaEntidad(usuario3);
    AbstractDAO.almacenaEntidad(usuario4);

//        Recuperamos 2 Usuarios
    AbstractDAO.getEntidad(usuario2.getId(), Usuario.class);
    AbstractDAO.getEntidad(usuario3.getId(), Usuario.class);

//        Actualizamos 3 Usuarios
    usuario1.setNombre("Nuevo nombre del Usuario1");
    usuario2.setNombre("Nuevo nombre del Usuario2");
    usuario3.setNombre("Nuevo nombre del Usuario3");
    AbstractDAO.almacenaEntidad(usuario1);
    AbstractDAO.almacenaEntidad(usuario2);
    AbstractDAO.almacenaEntidad(usuario3);

//        Eliminamos 2 Usuarios
    AbstractDAO.eliminaEntidad(usuario1);
    AbstractDAO.eliminaEntidad(usuario4);
}


Nuevamente el código es muy sencillo y autoexplicativo. Simplemente creamos 4 objetos "Usuario" que posteriormente almacenamos en la base de datos. Luego recuperamos 2 de estos Usuarios, haciendo uso de su identificador, actualizamos 3, y eliminamos 2, con lo que esperamos tener en consola 9 mensajes (2 de recuperación, 3 de actualización, 2 de antes de eliminar, y 2 de después de eliminar). Ejecutemos nuestra aplicación para ver la salida:



Como podemos ver, tenemos los 9 mensajes en consola que esperábamos, por lo que nuestros listeners de eventos han funcionado correctamente.

Ahora veamos cómo hacer lo mismo en código, directo en nuestra clase "HibernateUtil".


2.2. Configuración de Listeners de Eventos en Código


Si recordamos, "HibernateUtil" tiene un inicializador estático en donde se crea un objeto de tipo "org.hibernate.cfg.Configuration", si usamos archivos de mapeo, o "org.hibernate.cfg.AnnotationConfiguration", si usamos anotaciones. Actualmente creamos este objeto e inmediatamente, mediante encadenamiento de métodos, creamos el "SessionFactory" correspondiente:


new AnnotationConfiguration().configure().buildSessionFactory();


Como ahora necesitamos una referencia a este objeto para registrar nuestros listeners tendremos que modificar la línea anterior para hacer lo mismo, pero en dos pasos, de esta forma:

Configuration cfg = new AnnotationConfiguration();
sessionFactory = cfg.configure().buildSessionFactory();


Así ya tenemos una referencia a nuestro objeto "Configuration" para poder registrar los listeners de eventos que necesitamos. El objeto "org.hibernate.cfg.Configuration" tiene un método llamado "getEventListeners()", que regresa un objeto de tipo "org.hibernate.event.EventListeners", el cual mantendrá la lista de listeners de nuestra aplicación. Este objeto tiene una serie de métodos setters, uno para cada tipo de listeners, los cuales reciben un arreglo con los listeners de eventos que queremos registrar. Por ejemplo, para registrar el listener "CargaUsuarioListener", que recordemos que es de tipo "PostLoadEventListener", lo hacemos de la siguiente forma:

Configuration cfg = new AnnotationConfiguration();
EventListeners eventListeners = cfg.getEventListeners();
eventListeners.setPostLoadEventListeners(new PostLoadEventListener[]{new CargaUsuarioListener()});


Donde creamos un arreglo anónimo de tipo "PostLoadEventListener" cuyo único elemento es un objeto de tipo "CargaUsuarioListener". Su tuviéramos otro listener de tipo "PostLoadEventListener" simplemente lo agregaríamos dentro del mismo arreglo.

Podemos observar que este arreglo anónimo lo pasamos como parámetro al método "setPostLoadEventListeners" que es que usamos para registrar los listeners de tipo "PostLoadEventListener". Hacemos lo mismo con el resto de nuestros listeners de la siguiente forma:

eventListeners.setPreDeleteEventListeners (new PreDeleteEventListener[] {new PreEliminaUsuarioListener()});
eventListeners.setPostDeleteEventListeners(new PostDeleteEventListener[]{new PostEliminaUsuarioListener()});
eventListeners.setPreUpdateEventListeners (new PreUpdateEventListener[] {new ActualizaUsuarioListener()});


Con lo que la nueva clase "HibernateUtil" queda de la siguiente forma:

public class HibernateUtil
{
    private static final SessionFactory sessionFactory;

    static
    {
        try
        {
            Configuration cfg = new AnnotationConfiguration();
            EventListeners eventListeners = cfg.getEventListeners();
            
            eventListeners.setPostLoadEventListeners  (new PostLoadEventListener[]  {new CargaUsuarioListener()});
            eventListeners.setPreDeleteEventListeners (new PreDeleteEventListener[] {new PreEliminaUsuarioListener()});
            eventListeners.setPostDeleteEventListeners(new PostDeleteEventListener[]{new PostEliminaUsuarioListener()});
            eventListeners.setPreUpdateEventListeners (new PreUpdateEventListener[] {new ActualizaUsuarioListener()});
            
            sessionFactory = cfg.configure().buildSessionFactory();
        }
        catch (HibernateException he)
        {
            System.err.println("Ocurrió un error en la inicialización de la SessionFactory: " + he);
            throw new ExceptionInInitializerError(he);
        }
    }

    public static SessionFactory getSessionFactory()
    {
        return sessionFactory;
    }
}


Y listo. Ahora probemos que funciona colocando el siguiente código en el método "main" de nuestra clase "Main":

public static void main(String[] args)
{
    Usuario usuario1 = new Usuario("usuario 1", "username usuario1", "password usuario1");
    Usuario usuario2 = new Usuario("usuario 2", "username usuario2", "password usuario2");
    Usuario usuario3 = new Usuario("usuario 3", "username usuario3", "password usuario3");
    Usuario usuario4 = new Usuario("usuario 4", "username usuario4", "password usuario4");

//        Almacenamos las 4 entidades Usuario
    AbstractDAO.almacenaEntidad(usuario1);
    AbstractDAO.almacenaEntidad(usuario2);
    AbstractDAO.almacenaEntidad(usuario3);
    AbstractDAO.almacenaEntidad(usuario4);

//        Recuperamos 2 Usuarios
    AbstractDAO.getEntidad(usuario2.getId(), Usuario.class);
    AbstractDAO.getEntidad(usuario3.getId(), Usuario.class);

//        Actualizamos 3 Usuarios
    usuario1.setNombre("Nuevo nombre del Usuario1");
    usuario2.setNombre("Nuevo nombre del Usuario2");
    usuario3.setNombre("Nuevo nombre del Usuario3");
    AbstractDAO.almacenaEntidad(usuario1);
    AbstractDAO.almacenaEntidad(usuario2);
    AbstractDAO.almacenaEntidad(usuario3);

//        Eliminamos 2 Usuarios
    AbstractDAO.eliminaEntidad(usuario1);
    AbstractDAO.eliminaEntidad(usuario4);
}


Que es el mismo que ya había explicado. Solo recordemos que esperamos obtener en la consola 9 mensajes: 2 de recuperación, 3 de actualización, 2 de antes de eliminar, y 2 de después de eliminar. Cuando ejecutamos nuestra aplicación obtenemos la siguiente salida:



Como podemos ver, obtuvimos los 9 mensajes esperados. Por lo que nuestro ejemplo ha funcionado correctamente.

Pues bien, este fue el último tutorial oficial de la serie de Hibernate, los cuales espero sirvan para conocer los conceptos básicos de esta herramienta ORM que es bastante útil.

Cualquier duda, comentario, o sugerencia no duden en dejarlo en la sección correspondiente.

Saludos.

Descarga los archivos de este tutorial desde aquí:

Entradas Relacionadas:

16 comentarios:

  1. hola compa, buenos tutos, te comparto mi blog, donde hablo de java y voy de lo básico a lo avanzado, http://robertoleon.com.mx me gustaria ponerme en contacto contigo via msn o correo electronico saludos.

    rleon@sintelti.com.mx =)

    ResponderEliminar
  2. hola alex ps como siempre excelentes tus tutoriales!! ya mejor deberias escribir el libro y publicarlo!!!

    oye una pregunta que estrategia recomiendas para mapear una clase con llave compuesta???

    por que estoy viendo el manual de hibernate y dice que se puede utilizar el elemento y que hay sobrrescribir los metodos equal y hashCode pero a la vez dice que no recomienda esa estrategia en un sistema serio

    tons cual uso??? =(

    ResponderEliminar
  3. Hola Juan Carlos;

    Pues si, en realidad estoy de acuerdo con lo de no usar llaves compuestas para las entidadades =). Sin embargo, como tu mismo dices, existe una forma de hacer esto, aunque principalmente es para cuando tenemos bases de datos legadas, no nuevas (no se cual sea tu caso).

    Esto puede hacerse con un elemento llamado composite id.

    Aqui hay unos ejemplos de ellos:

    http://www.theserverside.com/discussions/thread.tss?thread_id=22638

    y aqui hay una explicación:

    http://docs.jboss.org/hibernate/core/3.3/reference/en/html/mapping.html#mapping-declaration-compositeid

    Espero que esto pueda ayudarte con tu problema.

    Slaudos :D

    ResponderEliminar
  4. hola Alex ps ya acabo de ver los links que pones y el primero no lo habia visto, el segundo fue donde te digo que vi que hibernate no recomienda esta manera pero entonces que so podria hacer o de que otra forma se podria mapear la clase cuando se tiene una tabla con una llave compuesta??

    ResponderEliminar
  5. Hola Alex oye tengo un problema (para varear)

    estoy configurando hibernate ahora en el modo "Open Session in View" con el siguiente filtro

    public class HibernateSessionRequestFilter implements Filter {

    private static Log log = LogFactory.getLog(HibernateSessionRequestFilter.class);

    private SessionFactory sf;

    public HibernateSessionRequestFilter() {

    }

    public void doFilter(ServletRequest request,
    ServletResponse response,
    FilterChain chain)
    throws IOException, ServletException {

    try {
    log.debug("Starting a database transaction");
    System.out.println("se llama el metodo doFilter");
    sf.getCurrentSession().beginTransaction();

    // Call the next filter (continue request processing)
    chain.doFilter(request, response);

    // Commit and cleanup
    log.debug("Committing the database transaction");
    sf.getCurrentSession().getTransaction().commit();

    } catch (StaleObjectStateException staleEx) {
    log.error("This interceptor does not implement optimistic concurrency control!");
    log.error("Your application will not work until you add compensation actions!");
    // Rollback, close everything, possibly compensate for any permanent changes
    // during the conversation, and finally restart business conversation. Maybe
    // give the user of the application a chance to merge some of his work with
    // fresh data... what you do here depends on your applicat ions design.
    throw staleEx;
    } catch (Throwable ex) {
    // Rollback only
    ex.printStackTrace();
    try {
    if (sf.getCurrentSession().getTransaction().isActive()) {
    log.debug("Trying to rollback database transaction after exception");
    sf.getCurrentSession().getTransaction().rollback();
    }
    } catch (Throwable rbEx) {
    log.error("Could not rollback transaction after exception!", rbEx);
    }
    finally {
    System.out.println("*** Cerrando la sesión *** ");
    sf.getCurrentSession().close();
    }

    // Let others handle it... maybe another interceptor for exceptions?
    throw new ServletException(ex);
    }
    }

    public void init(FilterConfig filterConfig) throws ServletException {
    log.debug("Initializing filter...");
    log.debug("Obtaining SessionFactory from static HibernateUtil singleton");
    System.out.println("se llama el metodo inir");
    sf = HibernateUtil.getSessionFactory();
    sf.getCurrentSession().beginTransaction();
    }

    public void destroy() {}

    }

    y desde un metodo mando llamar a HibernateUtil

    Session s = HibernateUtil.getSessionFactory().getCurrentSession();

    y despues

    s.save(unObjeto);

    pero me sigue marcando este error:

    org.hibernate.HibernateException: save is not valid without active transaction

    que podra ser????

    ResponderEliminar
  6. Hola Juan Carlos;

    No se porque no se ve tu comentario, pero me llego, asi que te conesto.

    Estas iniciando la transacción en un lado (en el metodo init) pero esa transacción no la usas, y luego en la ultima linea que me mandaste haces:

    Session s = HibernateUtil.getSessionFactory().getCurrentSession();

    s.save(unObjeto);

    Y ahi no inicias una transacción, ahi deberias hacer:

    Session s = HibernateUtil.getSessionFactory().getCurrentSession();

    s.beginTransaction();

    s.save(unObjeto);

    Prueba con ese cambio.

    Saludos

    ResponderEliminar
  7. si asi si funciona pero no se supone que ya tengo una transaccion activa en el filtro???

    como puedo obtener esa transaccion??? =( =(

    ResponderEliminar
  8. Hola Juan Carlos;

    Me pareceque de la forma en la que estas usando el Filtro el obtener la transacción no es tan directo. Tendrías que colocar la sesion como un atributo del request (request.setAttribute("sesion", sf.getCurrentSession();)

    y después en el componen que estas usando volver a recuperar la sesión para poder usarla.

    Saludos

    ResponderEliminar
  9. hola alex ,estoy realizando una aplicacion web con jsf e hibernate.Mi duda es cuando 2 clientes a la vez quieran visualizar los productos de la tienda, el cual muestra el numero de productos que hay disponible en stock de cada producto y quiera ambos comprarlo siendo la cantidad de producto disponible 1, uno de ellos no podra comprarlo.Para solucionar esto, he pensado utilizar patron singleton o hay algun mecanismo de hibernate que me pueda ayudar.saludos

    ResponderEliminar
  10. Amigo, gracias por los tutoriales.
    Logré llegar hasta el final de la Serie.

    ResponderEliminar
  11. Felicitaciones por estos tutoriales tan bien estructurados.

    Gracias por compartirnos tus conocimientos.

    ResponderEliminar
  12. Excelente tutorial, me ha servido mucho. Felicidades :)

    ResponderEliminar
  13. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  14. MAJESTUOSO tutorial!!! . Didáctico. Super bien explicado. En castellano. Ejemplos completos y con posibilidad de hacer consultas.
    Tenemos suerte, de contar con personas que no conocen el egoísmo a la hora de compartir los conocimientos.

    Eres todo un profesional.

    ResponderEliminar
  15. Muchísimas gracias por el tremendo tutorial, Álex.
    Aunque se ha quedado un poquillo desfasado (por lo visto con Hibernate 4 han cambiado un montón de cosas), me ha servido muchísimo para tener las ideas claras.
    Un cordial saludo.

    ResponderEliminar
  16. Hola amigo muy buen tutorial, tengo un gran problema, desearía que me ayudaras. como puedo mapear una vista en mysql con anotaciones y con xml. gracias

    ResponderEliminar