Hibernate – Como definir la forma de persistir nuestros objetos mediante la interfaz CompositeUserType.
0. Índice de contenidos.
1. Introducción
Trabajando con Hibernate podemos vernos en la necesidad de persistir un objeto de una forma específica,
para ello el framework nos ofrece una serie de interfaces que nos facilitan la tarea.
Tenemos dos opciones:
-
La interfaz «org.hibernate.usertype.UserType»
La clase que implementamos extendiendo dicha interfaz es la clase que sabe como serializar instancias de otra clase desde/hacia JDBC.
Solamente es capaz de definir tipos simples. -
La interfaz «org.hibernate.usertype.CompositeUserType»
Esta interfaz es una versión ampliada de la anterior, permitiendo definir «propiedades».
El uso de CompositeUserType esta asociado a la necesidad de tener más de una columna de base de datos para representar un objeto.
2. Entorno
El tutorial está escrito usando el siguiente entorno:
- Hardware: MacBook Pro 15′ (2.8 GHz Intel Core 2 Duo, 4GB DDR3 SDRAM).
- Sistema Operativo: Mac OS X Snow Leopard 10.6.3.
- NVIDIA GeForce 9600M GT 512Mb.
- Toshiba 500 Gb. 5400r.p.m.
3. CompositeUserType.
Vamos a utilizar «org.hibernate.usertype.CompositeUserType».
Las implementaciones deben ser inmutables y declarar un constructor público predeterminado.
Esta interfaz, debe aplicarse a campos anotados con @Type donde definimos el nombre de la clase que implementa la interfaz.
Ej. @Type(type = «com.autentia.xxx.yyy.StatusType»)
Imaginemos que tenemos una entidad cuenta que posee una máquina de estados con un estado actual y un estado anterior.
Los posibles estados son ACTIVE, UNCOVER, SUSPEND, CANCELED.
Estos estados son servicios de Spring que poseen la funcionalidad que implementa las transiciones desde este al resto de estados ante determinados eventos.
Para aislar la funcionalidad de la máquina de estados la encapsulamos en la clase StatusHolder,como no es el objetivo del tutorial no
profundizaremos en su implementación.
La entidad Account debe tener el siguiente atributo que nos permite persistir en BBDD los nombres de las clases que se encuentran asociadas al valor actual y anterior.
@Type(type = "com.autentia.xxx.yyy.StatusType") @Columns(columns = { @Column(name = "actual"), @Column(name = "previous") }) private StatusHolder statusHolder = new StatusHolder();
Pongo la clase completa como ejemplo de implementación.
package com.autentia.xxx.yyy.status; import java.io.Serializable; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import org.hibernate.Hibernate; import org.hibernate.HibernateException; import org.hibernate.engine.SessionImplementor; import org.hibernate.type.Type; import org.hibernate.usertype.CompositeUserType; import org.springframework.util.ClassUtils; import com.autentia.wuija.util.spring.SpringUtils; public class StatusType implements CompositeUserType { // nombre de las propiedades private String[] propertyNames = { "actual", "previous" }; // tipologia de las propiedades private Type[] propertyTypes = { Hibernate.STRING, Hibernate.STRING }; // Metodo para reconstruir un objeto desde la representación cacheable. @Override public Object assemble(Serializable cached, SessionImplementor session, Object owner) throws HibernateException { return deepCopy(cached); } // Metodo para transformar el objeto en su representación cacheable. @Override public Serializable disassemble(Object value, SessionImplementor session) throws HibernateException { return (Serializable)deepCopy(value); } // Devuelve una copia del objeto @Override public Object deepCopy(Object value) throws HibernateException { final StatusHolder originalStatusHolder = (StatusHolder)value; StatusHolder statusHolder = new StatusHolder(); statusHolder.setActual(originalStatusHolder.status()); statusHolder.setPrevious(originalStatusHolder.previousStatus()); return statusHolder; } @Override public boolean equals(Object x, Object y) throws HibernateException { return x.equals(y); } @Override public String[] getPropertyNames() { return propertyNames; } @Override public Type[] getPropertyTypes() { return propertyTypes; } @Override public Object getPropertyValue(Object component, int property) throws HibernateException { StatusHolder holder = (StatusHolder)component; switch (property) { case 0: return holder.status(); case 1: return holder.previousStatus(); } throw new IllegalArgumentException("property " + property + " recibido es un índice inválido para la clase " + ClassUtils.getQualifiedName(component.getClass())); } @Override public int hashCode(Object x) throws HibernateException { return x.hashCode(); } @Override public boolean isMutable() { return true; } // Metodo que construye una instancia de la clase asignada en base al resultado JDBC. @Override public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException { if (rs.wasNull()) { return null; } String actualName = rs.getString(names[0]); String previousName = rs.getString(names[1]); StatusHolder holder = null; holder = new StatusHolder(); try { Status actualState = getStatusByClassName(actualName); holder.setActual(actualState); Status previousState = getStatusByClassName(previousName); holder.setPrevious(previousState); } catch (Exception e) { throw new HibernateException(e); } return holder; } // Metodo que construye un PreparedStatement en base a la instancia pasada como parámetro. @Override public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException { if (value == null) { st.setNull(index, Hibernate.STRING.sqlType()); st.setNull(index+1, Hibernate.STRING.sqlType()); } else { StatusHolder holder = (StatusHolder)value; st.setString(index, ClassUtils.getQualifiedName(holder.status().getClass())); if ( holder.previousStatus() != null ){ st.setString(index+1, ClassUtils.getQualifiedName(holder.previousStatus().getClass())); } else { st.setNull(index+1, Hibernate.STRING.sqlType()); } } } @Override public Object replace(Object original, Object target, SessionImplementor session, Object owner) throws HibernateException { return deepCopy(original); } @Override public Class returnedClass() { return StatusHolder.class; } @Override public void setPropertyValue(Object component, int property, Object value) throws HibernateException { StatusHolder holder = (StatusHolder)component; String propertyValue = (String)value; Status status; try { status = getStatusByClassName(propertyValue); } catch (ClassNotFoundException e) { throw new IllegalArgumentException("Clase no encontrada", e); } switch (property) { case 0: holder.setActual(status); return; case 1: holder.setPrevious(status); return; } throw new IllegalArgumentException("property " + property + " recibido es un índice inválido para la clase " + ClassUtils.getQualifiedName(component.getClass())); } private Status getStatusByClassName(String previousName) throws ClassNotFoundException { return (Status) SpringUtils.getBean(Class.forName(previousName)); } }
4. Conclusión.
Como vemos en este ejemplo, simplemente implementando una interfaz («org.hibernate.usertype.CompositeUserType» en este caso) somos capaces de controlar el modo en el que
hibernate persiste nuestras clases.
Esto nos proporciona un mayor control y conocimiento sobre nuestro ORM.
Espero que os haya sido de utilidad.
Cualquier aclaración, duda o sugerencia podéis incluirla en la zona de comentarios.
Un saludo.
Alvaro Cuesta.