Eventos en Hibernate (parte III)

0
10421

Eventos en Hibernate (Parte III)

Índice de contenidos

  1. Introducción

  2. Entorno

  3. Un nuevo modelo

  4. Un DAO nuevo

  5. Configuración de Hibernate

  6. Un oyente muy atento

  7. Probar la aplicación

  8. Conclusiones

1. Introducción

Este tutorial es la tercera y última parte de la saga de
tutoriales sobre eventos en Hibernate. Si en la
primera parte vimos
cómo utilizar los oyentes de la SessionFactory y en la
segunda parte
utilizamos un EntityManager para poder utilizar las anotaciones de
EJB3, esta vez vamos a ver cómo combinar ambas cosas para
obtener lo mejor de dos mundos… Utilizaremos un oyente dentro de una
SessionFactory, que lo que hará será buscar anotaciones
EJB3 en los métodos de la entidad y sus superclases. En caso de
encontrarlas, ejecutará el método correspondiente.

Para nuestro ejemplo vamos a utilizar sólamente la anotación @PostLoad,
pero podríamos extenderlo fácilmente para cualquier
anotación y definir nuestros propios oyentes, de forma que una
vez definidos estos oyentes… ¡sólamente tendremos que
usar anotaciones para asociar métodos a los eventos, como
hacíamos con el EntityManager!.

2. Entorno

Para la realización de este tutorial se han utilizado
las siguientes herramientas:

  • Hardware: Portátil Asus G50Vseries (Core Duo
    P8600 2.4GHz, 4GB RAM, 320 GB HD).

  • Sistema operativo: Windows Vista Ultimate.

  • Eclipse Ganymede 3.4.1, con el plugin Q (http://code.google.com/p/q4e)
    para Maven

  • JDK 1.6.0

  • Maven 2.0.9

  • MySql 5.1.30

  • Hibernate 3

3. Un nuevo modelo

Partiremos del proyecto que
creamos en el primer tutorial, pero vamos a definir un nuevo modelo de
datos que utilice herencia. Crearemos una clase Transporte y dos
subclases que hereden de la misma.

Aquí está el código de la clase Transporte:

package modelo;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.PostLoad;

@Entity
public class Transporte {

	@Id
	@GeneratedValue
	private Integer id;
	
	private String tipo = "transporte";

	public Transporte() {
		super();
	}

	public Transporte(String tipo) {
		super();
		this.tipo = tipo;
	}

	public void viajar() {
		System.out.println("Viajando, viajando...");
	}
	
	@PostLoad
	private void repostar() {
		System.out.println("¡Necesito carburante!");
	}

	public Integer getId() {
		return id;
	}

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

	public String getTipo() {
		return tipo;
	}

	public void setTipo(String tipo) {
		this.tipo = tipo;
	}
}

Fijaros que hemos anotado el método «repostar»
con @PostLoad.
Además este método es privado, para comprobar que el
oyente pueda ejecutarlo sin importar su accesibilidad, utilizando
reflexión.

Y aquí tenemos TransporteTerrestre:

package modelo;

import javax.persistence.Entity;
import javax.persistence.PostLoad;

@Entity
public class TransporteTerrestre extends Transporte {

	private String nombre;
	
	public TransporteTerrestre() {
		super("Terrestre");
	}

	public TransporteTerrestre(String nombre) {
		super("Terrestre");
		this.nombre = nombre;
	}

	public void rodar() {
		System.out.println("Rodando vooooy");
	}
	
	@PostLoad
	public void derrapar() {
		System.out.println("¡Mira como derrapo en las curvas!");
	}


	public String getNombre() {
		return nombre;
	}


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

Aquí hemos anotado el método «derrapar».

Y, por último, TransporteAereo:

package modelo;

import javax.persistence.Entity;

@Entity
public class TransporteAereo extends Transporte {

	private Integer capacidad;
	
	public TransporteAereo() {
		super("Aereo");
	}
	
	public TransporteAereo(Integer capacidad) {
		super("Aereo");
		this.capacidad = capacidad;
	}

	public void volar() {
		System.out.println("Volando vooooy");
	}

	public void aterrizar() {
		System.out.println("Aterriza como puedas");
	}

	public Integer getCapacidad() {
		return capacidad;
	}

	public void setCapacidad(Integer capacidad) {
		this.capacidad = capacidad;
	}
}

Esta
vez no hemos anotado ningún método a propósito,
para que nuestro oyente llame al método anotado en la clase
padre.

4. Un DAO nuevo

El Dao que habíamos utilizado en el ejemplo anterior
tampoco nos
vale ahora, porque hemos cambiado el modelo. No obstante las diferencias van a ser mínimas.

package modelo;

import java.util.ArrayList;
import java.util.List;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.AnnotationConfiguration;

public class TransporteDao {

	private static SessionFactory sf = new AnnotationConfiguration().configure("hibernate.cfg.xml").buildSessionFactory();
	
	public void guardarTransporte(Transporte transporte) {
		Session sesion = sf.openSession();
		Transaction tx = null;

		try {
			tx = sesion.beginTransaction();
			sesion.save(transporte);
			tx.commit();
		} catch (Exception e) {
			e.printStackTrace();
			tx.rollback();
		} finally {
			sesion.close();
		}
	}
	
	public List buscarTransportes() {
		List transportes = new ArrayList();
		
		Session sesion = sf.openSession();
		Transaction tx = null;;

		try {
			tx = sesion.beginTransaction();
			transportes = sesion.createQuery("from Transporte").list();
			tx.commit();
		} catch (Exception e) {
			e.printStackTrace();
			tx.rollback();
		} finally {
			sesion.close();
		}

		return transportes;
	}	
}

5. Configuración de Hibernate

Ahora
vamos a ver cómo quedaría el fichero de
configuración de Hibernate, que utiliza el nuevo modelo de
clases.




    
    	org.hibernate.dialect.MySQLInnoDBDialect
        com.mysql.jdbc.Driver
        root
        root
        jdbc:mysql://localhost:3306/eventos?autoReconnect=true

		update
		true

		
		
		
		
		
			
			
		
	


Como
se puede observar, hemos añadido el
oyente AnotacionPostLoadOyente para el evento PostLoad. Ya
sólo nos falta implementar el oyente y podremos probar nuestra
aplicación.

6. Un oyente muy atento

Nuestro oyente
utilizará reflexión para buscar anotaciones @PostLoad en
los métodos de la entidad que lo ha provocado y en sus
superclases hasta llegar a Object. Si encuentra un método
anotado, la búsqueda se detendrá y se ejecutará el
método. Además, la clase utiliza un Map
en el que irá guardando los métodos asociados a cada
clase sobre la que busque, de manera que no tenga que repetir una y
otra vez las búsquedas para una misma
clase.

¡OJO! El hecho de que utilicemos un Map para
guardar cierto estado sobre las búsquedas anteriores, hace que
está función NO SEA THREADSAFE por lo que NO ES RECOMENDABLE UTILIZARLA EN UN ENTORNO DE PRODUCCIÓN tal cual está.

Con este oyente estaremos simulando la funcionalidad que
nos ofrecen los oyentes del EntityManager, pero sólamente
utilizando una SessionFactory, lo cual es una buena idea en algunos
casos. Lástima que no se me haya ocurrido a mí 😉

Aquí tenemos el código de nuestro atentísimo oyente:

package oyentes;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import javax.persistence.PostLoad;

import org.hibernate.event.PostLoadEvent;
import org.hibernate.event.PostLoadEventListener;

public class AnotacionPostLoadOyente implements PostLoadEventListener
{
	private static final long serialVersionUID = 4822764284443093197L;

	// Guarda el metodo a ejecutar por cada clase de entidad para no tener
	// que repetir en cada llamada la busqueda de anotaciones para dicha clase
	private static Map, Method> metodosAnotados = new HashMap, Method>();

	@SuppressWarnings("unused")
	private void metodoNulo() {}

	private static Method metodoNulo = null;

	static {
		Class miClase = AnotacionPostLoadOyente.class;
		try {
			metodoNulo = miClase.getDeclaredMethod("metodoNulo");

		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public void onPostLoad(PostLoadEvent event)
	{
		Object entidad = event.getEntity();
		ejecutarMetodoAnotado(entidad, PostLoad.class);
	}

	private Method buscarMetodoAnotadoRecursivo(Class clase, Class anotacion) {
		Method metodo = metodosAnotados.get(clase);
		
		if (metodo != null) {
			return metodo;
		}
		
		if (clase == Object.class) {
			metodo = metodoNulo;
		} else {
			for (Method m : clase.getDeclaredMethods()) {
				System.out.println("Examinando metodo: " + m.getName());
	
				if (m.getAnnotation(anotacion) != null) {
					metodo = m;
					break;
				}
			}

			if (metodo == null) {
				metodo = buscarMetodoAnotadoRecursivo(clase.getSuperclass(), anotacion);
			}
		}

		System.out.println("Guardando el metodo " + clase.getSimpleName() + "." + metodo.getName());
		metodosAnotados.put(clase, metodo);
		
		return metodo;
	}


	private void ejecutarMetodoAnotado(Object entidad, Class anotacion) {
		System.out.println("-------------------------------------");
		System.out.println("Disparado oyente para la clase " + entidad.getClass().getSimpleName());

		Method metodo = buscarMetodoAnotadoRecursivo(entidad.getClass(), anotacion);

		if (metodo != metodoNulo) { // Si existe un metodo anotado lo invoca
			try {
				boolean accesible = metodo.isAccessible();
				metodo.setAccessible(true); // Hacemos el metodo accesible
				metodo.invoke(entidad);
				metodo.setAccessible(accesible); // Restauramos la accesibilidad del metodo
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

La clase auxiliar «buscarMetodoAnotadoRecursivo» es una función recursiva que comprueba
si la clase se ha buscado anteriormente o no. En el primer caso
devuelve directamente el metodo a ejecutar (o una referencia a metodoNulo si no hay ninguno).

En caso de que la clase no se haya buscado anteriormente
comprueba que no se trata de la clase Object (a la cual se
llegará si no se encuentra ningún método anotado)
y busca la anotación en todos los métodos de la clase. Si
encuentra el método detiene la búsqueda, pero en caso
contrario repite el proceso para la clase padre, y así
recursivamente, hasta encontrar el método o llegar a Object.

En cualquier caso, si el método no estaba ya guardado se guardará en el mapa para no tener que repetir búsquedas para las mismas clases en el futuro.

7. Probar la aplicación

Ahora sólamente nos queda probar la
aplicación que hemos rehecho por tercera vez, para ver si funciona como es
debido. Para ello modificamos nuestras dos clases de prueba para que
utilicen el nuevo modelo de datos.

Aquí está el código modificado de
nuestra
clase PruebaInsercion, que guardará en la base de datos dos transportes de cada tipo.

import modelo.TransporteTerrestre;

public class PruebaInsercion {
	public static void main(String[] args) {
		TransporteDao dao = new TransporteDao();
		Transporte transporte = new Transporte();
		dao.guardarTransporte(transporte);
		transporte = new Transporte();
		dao.guardarTransporte(transporte);
		transporte = new TransporteTerrestre("Coche");
		dao.guardarTransporte(transporte);
		transporte = new TransporteTerrestre("Moto");
		dao.guardarTransporte(transporte);
		transporte = new TransporteAereo(300);
		dao.guardarTransporte(transporte);
		transporte = new TransporteAereo(100);
		dao.guardarTransporte(transporte);
	}
}

Y aquí tenemos la clase PruebaListado, que utiliza
también el nuevo Dao.

package prueba;

import java.util.List;

import modelo.Transporte;
import modelo.TransporteDao;

public class PruebaListado {
	public static void main(String[] args) {
		TransporteDao dao = new TransporteDao();
		List transportes = dao.buscarTransportes();
		
		System.out.println("Hay " + transportes.size() + " transportes");
		
		for (Transporte transporte : transportes) {
			System.out.println("Transporte = " + transporte.getTipo());
		}
	}
}

Ejecutamos primero la inserción y comprobamos que se
han añadido correctamente los transportes a nuestra base de datos:

Listado de transportes en la base de datos

Ahora ejecutamos la prueba de listado y… ¡A la tercera va la
vencida! Esta vez nuestro oyente sí que estaba escuchando.

Salida por consola

Para
la primera clase de tipo transporte se ha realizado la búsqueda
de anotaciones y se ha ejecutado el método «repostar». Para la
segunda, como ya se había buscado anteriormente, se ha ejecutado
el método directamente.

Lo mismo ocurre para el transporte terrestre, aunque esta vez el método encontrado es «derrapar».

Para
el transporte aéreo se ha realizado inicialmente la
búsqueda y, como no habíamos anotado ningún
método de la clase, se ha utilizado el método «repostar»
de la clase padre, el cual ya estaba guardado en el mapa. La segunda
vez no ha sido necesario tampoco repetir la búsqueda para
TransporteAereo.

10. Conclusiones

A la vista de los resultados de este tutorial, podemos extraer
las siguientes conclusiones:

  • Con esta técnica podemos construir unos oyentes
    predeterminados para cualquier SessionFactory y después utilizar
    sólamente anotaciones para definir nuestros métodos de
    retrollamada.

  • Con un poco de imaginación y pericia casi siempre podemos
    encontrar una buena solución para resolver nuestros problemas.

Y esto es todo. Aquí termina nuestra serie de tutoriales
sobre eventos en Hibernate. Espero que os guste tanto seguirla como a
mí haberla hecho.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad