miércoles, 30 de mayo de 2012

6.4 Manejo de objetos persistentes

Las clases presistentes son clases en una aplicación que implementan las entidades del problema empresarial (por ejemplo, Customer y Order en una aplicación de comercio electrónico). No se considera que todas las instancias de una clase persistente estén en estado persistente. Por ejemplo, una instancia puede ser transitoria o separada.

Hibernate funciona mejor si estas clases siguen algunas reglas simples, también conocidas como el modelo de programación POJO (Plain Old Java Object). Sin embargo, ninguna de estas reglas son requerimientos rígidos. De hecho, Hibernate3 asume muy poco acerca de la naturaleza de sus objetos persistentes. Puede expresar un modelo de dominio en otras formas (por ejemplo, utilizando árboles de instancias de Map).

Ejemplo simple de POJO

La mayoría de aplicaciones Java requieren una clase persistente que represente a los felinos. Por ejemplo:

package eg;

import java.util.Set;

import java.util.Date;



public class Cat {

private Long id; // identifier

private Date birthdate;

private Color color;

private char sex;

private float weight;

private int litterId;

private Cat mother;

private Set kittens = new HashSet();

private void setId(Long id) {

this.id=id;

}

public Long getId() {

return id;

}

void setBirthdate(Date date) {

birthdate = date;

}

public Date getBirthdate() {

return birthdate;

}

void setWeight(float weight) {

this.weight = weight;

}

public float getWeight() {

return weight;

}

public Color getColor() {

return color;

}

void setColor(Color color) {

this.color = color;

}

void setSex(char sex) {

this.sex=sex;

}

public char getSex() {

return sex;

}

void setLitterId(int id) {

this.litterId = id;

}

public int getLitterId() {

return litterId;

}

void setMother(Cat mother) {

this.mother = mother;

}

public Cat getMother() {

return mother;

}

void setKittens(Set kittens) {

this.kittens = kittens;

}

public Set getKittens() {

return kittens;

}

public void addKitten(Cat kitten) {

kitten.setMother(this);

kitten.setLitterId( kittens.size() );

kittens.add(kitten);

}

}

En las siguientes secciones vamos a explorar en mayor detalle las cuatro reglas principales de las clases persistentes.

Implemente un constructor sin argumentos

Cat tiene un contructor sin argumentos. Todas las clases persistentes deben tener un constructor predeterminado (el cual puede ser no-público) de modo que Hibernate pueda instanciarlas usando Constructor.newInstance(). Le recomendamos contar con un constructor por defecto con al menos una visibilidad de paquete para la generación de proxies en tiempo de ejecución en Hibernate.

Proporcione una propiedad identificadora (opcional)

Cat tiene una propiedad llamada id. Esta propiedad mapea a la columna de la llave principal de la tabla de la base de datos. La propiedad podría llamarse de cualquier manera y su tipo podría haber sido cualquier tipo primitivo, cualquier tipo de "wrapper" primitivo, java.lang.String o java.util.Date. Si su tabla de base de datos heredada tiene claves compuestas, puede utilizar una clase definida por el usuario con propiedades de estos tipos

La propiedad identificadora es estrictamente opcional. Puede olvidarla y dejar que Hibernate siga internamente la pista de los identificadores del objeto. Sin embargo, no recomendamos que esto suceda.

De hecho, algunas funcionalidades se encuentran disponibles sólamente para clases que declaran una propiedad identificadora:

  • Session.saveOrUpdate()
  • Session.merge()

Le recomendamos que declare propiedades identificadoras nombradas-consistentemente en clases persistentes. y que utilice un tipo nulable (por ejemplo, no primitivo).

Prefiera las clases no finales

Un aspecto central de Hibernate, los proxies, dependen de que las clases persistentes sean no finales o de la implementación de una interfaz que declare todos los métodos públicos.

Con Hibernate puede persistir las clases finales que no implementen una interfaz. Sin embargo, no podrá utilizar proxies para recuperación perezosa de asociaciones, lo cual limitará sus opciones para afinar el rendimiento.

También debe evitar el declarar métodos public final en las clases no-finales. Si quiere utilizar una clase con un método public final, debe deshabilitar explícitamente el uso de proxies estableciendo lazy="false".

Declare métodos de acceso y de modificación para los campos persistentes

Cat declara métodos de acceso para todos sus campos persistentes. Muchas otras herramientas ORM persisten directamente variables de instancia. Es mejor proporcionar una indirección entre el esquema relacional y las estructuras de datos internos de la clase. Por defecto, Hibernate persiste las propiedades del estilo JavaBeans, y reconoce los nombres de método de la forma getFoo, isFoo y setFoo. De ser necesario, puede cambiarse al acceso directo a campos para propiedades específicas.

No es necesario declarar públicas las propiedades. Hibernate puede persistir una propiedad con un par get / set protected o private.



Implementación de herencia

Una subclase también tiene que cumplir con la primera y la segunda regla. Hereda su propiedad identificadora de la superclase Cat. Por ejemplo:

package eg;

public class DomesticCat extends Cat {

private String name;

public String getName() {

return name;

}

protected void setName(String name) {

this.name=name;

}

}

Implementando equals() y hashCode()

Tiene que sobrescribir los métodos equals() y hashCode() si:

  • piensa poner instancias de clases persistentes en un Set (la forma recomendada de representar asociaciones multivaluadas); y
  • piensa utilizar reasociación de instancias separadas.

Hibernate garantiza la equivalencia de identidad persistente (fila de base de datos) y de identidad Java sólamente dentro del ámbito de una sesión en particular. De modo que en el momento en que mezcla instancias recuperadas en sesiones diferentes, tiene que implementar equals() y hashCode() si desea tener una semántica significativa para Sets.

La forma más obvia es implementar equals()/hashCode() comparando el valor identificador de ambos objetos. Si el valor es el mismo, ambos deben ser la misma fila de la base de datos ya que son iguales. Si ambos son agregados a un Set, sólo tendremos un elemento en el Set). Desafortunadamente, no puede utilizar este enfoque con identificadores generados. Hibernate sólo asignará valores identificadores a objetos que son persistentes; una instancia recién creada no tendrá ningún valor identificador. Además, si una instancia no se encuentra guardada y está actualmente en un Set, al guardarla se asignará un valor identificador al objeto. Si equals() y hashCode() están basados en el valor identificador, el código hash podría cambiar, rompiendo el contrato del Set. Consulte el sitio web de Hibernate y allí encontrará una discusión completa sobre este problema. Este no es un problema de Hibernate, sino de la semántica normal de Java de identidad de objeto e igualdad.

Le recomendamos implementar equals() y hashCode() utilizando igualdad de clave empresarial (Business key equality). Igualdad de clave empresarial significa que el método equals() sólamente compara las propiedades que forman la clave empresarial. Esta es una clave que podría identificar nuestra instancia en el mundo real (una clave candidata natural):



public class Cat {

public boolean equals(Object other) {

if (this == other) return true;

if ( !(other instanceof Cat) ) return false;

final Cat cat = (Cat) other;

if ( !cat.getLitterId().equals( getLitterId() ) ) return false;

if ( !cat.getMother().equals( getMother() ) ) return false;

return true;

}

public int hashCode() {

int result;

result = getMother().hashCode();

result = 29 * result + getLitterId();

return result;

}

}

Modelos dinámicos

Las entidades persistentes no necesariamente tienen que estar representadas como clases POJO o como objetos JavaBean en tiempo de ejecución. Hibernate también soporta modelos dinámicos (utilizando Mapeos de Mapeos en tiempo de ejecución) y la representación de entidades como árboles de DOM4J. No escriba clases persistentes con este enfoque, sólamente archivos de mapeo.

Los siguientes ejemplos demuestran la representación utilizando Mapeos. Primero, en el archivo de mapeo tiene que declararse un entity-name en lugar de, o además de un nombre de clase:

<hibernate-mapping>

<class entity-name="Customer">

<id name="id"

type="long"

column="ID">

<generator class="sequence"/>

</id>



<property name="name"

column="NAME"

type="string"/>



<property name="address"

column="ADDRESS"

type="string"/>



<many-to-one name="organization"

column="ORGANIZATION_ID"

class="Organization"/>



<bag name="orders"

inverse="true"

lazy="false"

cascade="all">

<key column="CUSTOMER_ID"/>

<one-to-many class="Order"/>

</bag>



</class>


</hibernate-mapping

>

Aunque las asociaciones se declaran utilizando nombres de clase destino, el tipo destino de una asociación puede ser además una entidad dinámica en lugar de un POJO.

Después de establecer el modo de entidad predeterminado como dynamic-map para la SessionFactory, puede trabajar en tiempo de ejecución con Mapeos de Mapeos:



Session s = openSession();

Transaction tx = s.beginTransaction();

// Crea un cliente

Map david = new HashMap();

david.put("name", "David");



// Crea una organizacion

Map foobar = new HashMap();

foobar.put("name", "Foobar Inc.");



// Liga ambos

david.put("organization", foobar);



// Guarda ambos

s.save("Customer", david);

s.save("Organization", foobar);



tx.commit();

s.close();

Una de las ventajas principales de un mapeo dinámico es el rápido tiempo de entrega del prototipado sin la necesidad de implementar clases de entidad. Sin embargo, pierde el chequeo de tipos en tiempo de compilación y muy probablemente tendrá que tratar con muchas excepciones en tiempo de ejecución. Gracias al mapeo de Hibernate, el esquema de base de datos se puede normalizar y volver sólido, permitiendo añadir una implementación apropiada del modelo de dominio más adelante.

Los modos de representación de entidad se pueden establecer por Session:

Session dynamicSession = pojoSession.getSession(EntityMode.MAP);



// Crea un cliente

Map david = new HashMap();

david.put("name", "David");

dynamicSession.save("Customer", david);

...

dynamicSession.flush();

dynamicSession.close()

...

// Continua en pojoSession

Tenga en cuenta que la llamada a getSession() utilizando un EntityMode está en la API de Session, no en la de SessionFactory. De esta forma, la nueva Session comparte la conexión JDBC, la transacción y otra información de contexto. Esto significa que no tiene que llamar a flush() ni a close() en la Session secundaria, y también tiene que dejar el manejo de la transacción y de la conexión a la unidad de trabajo primaria.

Tuplizers

org.hibernate.tuple.Tuplizer y sus subinterfaces son las responsables de administrar una representación en particular de un dato, dadas las representaciones de org.hibernate.EntityMode. Si un dato dado se considera como una estructura de datos entonces un tuplizer es la cosa que sabe como crear tal estructura de datos y sabe como extraer e insertar valores en dicha estructura de datos. Por ejemplo, para el modo de entidad POJO, el tuplizer correspondiente sabe como crear el POJO por medio de su constructor. También sabe como acceder a las propiedades POJO utilizando los accesores de propiedad definidos.



Hay dos tipos altos de niveles de Tuplizers, representados por las interfaces org.hibernate.tuple.entity.EntityTuplizer y org.hibernate.tuple.component.ComponentTuplizer. Los EntityTuplizers son los responsables de administrar los contratos mencionados anteriormente en relación con las entidades mientras que los ComponentTuplizers hacen lo mismo para los componentes.

Los usuarios también pueden enchufar sus propios tuplizers. Tal vez necesite que una implementación java.util.Map diferente de java.util.HashMap se utilice en el modo de entidad de mapeo dinámico. O quizás necesite definir una estrategia de generación proxy diferente de la que se utiliza por defecto. Se pueden obtener ambas al definir una implementación tuplizer personalizada. Las definiciones de los tuplizers se encuentran sujetas a la entidad o componente de mapeo que se supone que tienen que administrar. Regresando al ejemplo de nuestra entidad de cliente:



<hibernate-mapping>

<class entity-name="Customer">

<!--

Override the dynamic-map entity-mode

tuplizer for the customer entity

-->

<tuplizer entity-mode="dynamic-map"

class="CustomMapTuplizerImpl"/>



<id name="id" type="long" column="ID">

<generator class="sequence"/>

</id>



<!-- other properties -->

...

</class>

</hibernate-mapping>



public class CustomMapTuplizerImpl extends org.hibernate.tuple.entity.DynamicMapEntityTuplizer{

// reemplaza el método buildInstantiator () para conectar a nuestro mapa personalizado…

protected final Instantiator buildInstantiator(

org.hibernate.mapping.PersistentClass mappingInfo) {

return new CustomMapInstantiator( mappingInfo );

}



private static final class CustomMapInstantiator

extends org.hibernate.tuple.DynamicMapInstantitor {

// reemplaza el método generateMap() para regresar nuestro mapa personalizado...

protected final Map generateMap() {

return new CustomMap();

}

}

}

EntityNameResolvers

La interfaz org.hibernate.EntityNameResolver es un contrato para resolver el nombre de la entidad de una instancia de entidad dada. La interfaz define un solo método resolveEntityName, el cual se le pasa la instancia entidad y se espera que retorne el nombre de entidad apropriado (se permite nulo e indicaría que el resolvedor no sabe cómo resolver el nombre de la entidad de la instancia de entidad dada). Generalmente hablando, un org.hibernate.EntityNameResolver será más útil en el caso de modelos dinámicos. Un ejemplo puede ser el usar interfaces con proxis como su modelo de dominio. La suite de prueba de hibernate tiene un ejemplo de este estilo exacto de uso bajo el org.hibernate.test.dynamicentity.tuplizer2. Aquí está algo del código de ese paquete para su ilustración.

public final class DataProxyHandler implements InvocationHandler {

private String entityName;

private HashMap data = new HashMap();



public DataProxyHandler(String entityName, Serializable id) {

this.entityName = entityName;

data.put( "Id", id );

}



public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

String methodName = method.getName();

if ( methodName.startsWith( "set" ) ) {

String propertyName = methodName.substring( 3 );

data.put( propertyName, args[0] );

}

else if ( methodName.startsWith( "get" ) ) {

String propertyName = methodName.substring( 3 );

return data.get( propertyName );

}

else if ( "toString".equals( methodName ) ) {

return entityName + "#" + data.get( "Id" );

}

else if ( "hashCode".equals( methodName ) ) {

return new Integer( this.hashCode() );

}

return null;

}



public String getEntityName() {

return entityName;

}



public HashMap getData() {

return data;

}

}



public class ProxyHelper {

public static String extractEntityName(Object object) {

if ( Proxy.isProxyClass( object.getClass() ) ) {

InvocationHandler handler = Proxy.getInvocationHandler( object );

if ( DataProxyHandler.class.isAssignableFrom( handler.getClass() ) ) {

DataProxyHandler myHandler = ( DataProxyHandler ) handler;

return myHandler.getEntityName();

}

}

return null;

}



// Otros metodos utiles....



}



public class MyEntityNameResolver implements EntityNameResolver {

public static final MyEntityNameResolver INSTANCE = new MyEntityNameResolver();



public String resolveEntityName(Object entity) {

return ProxyHelper.extractEntityName( entity );

}



public boolean equals(Object obj) {

return getClass().equals( obj.getClass() );

}



public int hashCode() {

return getClass().hashCode();

}

}



public class MyEntityTuplizer extends PojoEntityTuplizer {

public MyEntityTuplizer(EntityMetamodel entityMetamodel, PersistentClass mappedEntity) {

super( entityMetamodel, mappedEntity );

}



public EntityNameResolver[] getEntityNameResolvers() {

return new EntityNameResolver[] { MyEntityNameResolver.INSTANCE };

}



public String determineConcreteSubclassEntityName(Object entityInstance, SessionFactoryImplementor factory) {

String entityName = ProxyHelper.extractEntityName( entityInstance );

if ( entityName == null ) {

entityName = super.determineConcreteSubclassEntityName( entityInstance, factory );

}

return entityName;

}



...

}