Hibernate.orgCommunity Documentation

Capítulo 11. Trabajo con objetos

11.1. Estados de objeto de Hibernate
11.2. Haciendo los objetos persistentes
11.3. Cargando un objeto
11.4. Consultas
11.4.1. Ejecución de consultas
11.4.2. Filtración de colecciones
11.4.3. Consultas de criterios
11.4.4. Consultas en SQL nativo
11.5. Modificación de objetos persistentes
11.6. Modificación de objetos separados
11.7. Detección automática de estado
11.8. Borrado de objetos persistentes
11.9. Replicación de objetos entre dos almacenamientos de datos diferentes
11.10. Limpieza (flushing) de la sesión
11.11. Persistencia transitiva
11.12. Utilización de metadatos

Hibernate es una solución completa de mapeo objeto/relacional que no sólo proteje al desarrollador de los detalles del sistema de administración de la base datos subyacente, sino que además ofrece administración de estado de objetos. Contrario a la administración de declaraciones SQL en capas comunes de persistencia JDBC/SQL, esta es una vista natural orientada a objetos de la persistencia en aplicaciones Java.

En otras palabras, los desarrolladores de aplicaciones de Hibernate siempre deben pensar en el estado de sus objetos, y no necesariamente en la ejecución de declaraciones SQL. Hibernate se ocupa de esto y es sólamente relevante para el desarrollador de la aplicación al afinar el rendimiento del sistema.

Hibernate define y soporta los siguientes estados de objeto:

Discutiremos ahora los estados y transiciones de estados (y los métodos de Hibernate que disparan una transición) en más detalle.

Las instancias recién instanciadas de una clase persistente, Hibernate las considera como transitorias. Podemos hacer una instancia transitoria persistente asociándola con una sesión:

DomesticCat fritz = new DomesticCat();

fritz.setColor(Color.GINGER);
fritz.setSex('M');
fritz.setName("Fritz");
Long generatedId = (Long) sess.save(fritz);

Si Cat tiene un identificador generado, el identificador es generado y asignado al cat cuando se llama a save(). Si Cat tiene un identificador assigned, o una clave compuesta, el identificador debe ser asignado a la instancia de cat antes de llamar a save(). También puede utilizar persist() en vez de save(), con la semántica definida en el borrador de EJB3.

Opcionalmente, puede asignar el identificador utilizando una versión sobrecargada de save().

DomesticCat pk = new DomesticCat();

pk.setColor(Color.TABBY);
pk.setSex('F');
pk.setName("PK");
pk.setKittens( new HashSet() );
pk.addKitten(fritz);
sess.save( pk, new Long(1234) );

Si el objeto que hace persistente tiene objetos asociados (por ejemplo, la colección kittens en el ejemplo anterior), estos objetos pueden ser hechos persistentes en cualquier orden que quiera a menos de que tenga una restricción NOT NULL sobre una columna clave foránea. Nunca hay riesgo de violar restricciones de clave foránea. Sin embargo, puede que usted viole una restricción NOT NULL si llama a save() sobre los objetos en el orden equivocado.

Usualmente no se preocupe de este detalle, pues muy probablemente utilizará la funcionalidad de persistencia transitiva de Hibernate para guardar los objetos asociados automáticamente. Entonces, ni siquiera tienen lugar violaciones de restricciones NOT NULL - Hibernate se ocupará de todo. Más adelante en este capítulo se discute la persistencia transitiva.

Los métodos load() de Session le proporcionan una forma de recuperar una instancia persistente si ya conoce su identificador. load() toma un objeto clase y carga el estado dentro de una instancia recién instanciada de esa clase, en un estado persistente.

Cat fritz = (Cat) sess.load(Cat.class, generatedId);
// you need to wrap primitive identifiers

long id = 1234;
DomesticCat pk = (DomesticCat) sess.load( DomesticCat.class, new Long(id) );

Alternativamente, puede cargar estado dentro de una instancia dada:

Cat cat = new DomesticCat();

// load pk's state into cat
sess.load( cat, new Long(pkId) );
Set kittens = cat.getKittens();

Note que load() lanzará una excepción irrecuperable si no hay una fila correspondiente en la base de datos. Si la clase se mapea con un proxy, load() sólo retorna un proxy no inicializado y no llamará realmente a la base de datos hasta que invoque un método del proxy. Este comportamiento es muy útil si desea crear una asociación a un objeto sin cargarlo realmente de la base de datos. Además permite que múltiples instancias sean cargadas como un lote si se define batch-size para el mapeo de la clase.

Si no tiene la certeza de que existe una fila correspondiente, debe utilizar el método get(), que llama a la base de datos inmediatamente y devuelve nulo si no existe una fila correspondiente.

Cat cat = (Cat) sess.get(Cat.class, id);

if (cat==null) {
    cat = new Cat();
    sess.save(cat, id);
}
return cat;

Incluso puede cargar un objeto utilizando un SELECT ... FOR UPDATE de SQL, usando un LockMode. Consulte la documentación de la API para obtener más información.

Cat cat = (Cat) sess.get(Cat.class, id, LockMode.UPGRADE);

Ninguna instancia asociada o colección contenida es seleccionada para actualizacion - FOR UPDATE, a menos de que decida especificar lock o all como un estilo de cascada para la asociación.

Es posible volver a cargar un objeto y todas sus colecciones en cualquier momento, utilizando el método refresh(). Esto es útil cuando se usan disparadores de base de datos para inicializar algunas de las propiedades del objeto.

sess.save(cat);

sess.flush(); //force the SQL INSERT
sess.refresh(cat); //re-read the state (after the trigger executes)

How much does Hibernate load from the database and how many SQL SELECTs will it use? This depends on the fetching strategy. This is explained in Sección 21.1, “Estrategias de recuperación”.

Si no conoce los identificadores de los objetos que está buscando, necesita una consulta. Hibernate soporta un lenguaje de consulta orientado a objetos (HQL) fácil de usar pero potente a la vez. Para la creación de consultas programáticas, Hibernate soporta una funcionalidad sofisticada de consulta de Criteria y Example (QBC y QBE). También puede expresar su consulta en el SQL nativo de su base de datos, con soporte opcional de Hibernate para la conversión del conjunto de resultados a objetos.

Las consultas HQL y SQL nativas son representadas con una instancia de org.hibernate.Query. Esta interfaz ofrece métodos para ligar parámetros, manejo del conjunto resultado, y para la ejecución de la consulta real. Siempre obtiene una Query utilizando la Session actual:

List cats = session.createQuery(

    "from Cat as cat where cat.birthdate < ?")
    .setDate(0, date)
    .list();
List mothers = session.createQuery(
    "select mother from Cat as cat join cat.mother as mother where cat.name = ?")
    .setString(0, name)
    .list();
List kittens = session.createQuery(
    "from Cat as cat where cat.mother = ?")
    .setEntity(0, pk)
    .list();
Cat mother = (Cat) session.createQuery(
    "select cat.mother from Cat as cat where cat = ?")
    .setEntity(0, izi)
    .uniqueResult();]]
Query mothersWithKittens = (Cat) session.createQuery(
    "select mother from Cat as mother left join fetch mother.kittens");
Set uniqueMothers = new HashSet(mothersWithKittens.list());

Una consulta se ejecuta usualmente invocando a list(). El resultado de la consulta será cargado completamente dentro de una colección en memoria. Las instancias de entidad recuperadas por una consulta se encuentran en estado persistente. El método uniqueResult() ofrece un atajo si sabe que su consulta retornará sólamente un objeto. Las consultas que hacen uso de una recuperación temprana de colecciones usualmente retornan duplicados de los objetos raíz, pero con sus colecciones inicializadas. Puede filtrar estos duplicados a través de un Set.

Queries can also be configured as so called named queries using annotations or Hibernate mapping documents. @NamedQuery and @NamedQueries can be defined at the class level as seen in Ejemplo 11.1, “Defining a named query using @NamedQuery” . However their definitions are global to the session factory/entity manager factory scope. A named query is defined by its name and the actual query string.


Using a mapping document can be configured using the <query> node. Remember to use a CDATA section if your query contains characters that could be interpreted as markup.


Parameter binding and executing is done programatically as seen in Ejemplo 11.3, “Parameter binding of a named query”.


El código real del programa es independiente del lenguaje de consulta utilizado. También puede definir consultas SQL nativas en metadatos, o migrar consultas existentes a Hibernate colocándolas en archivos de mapeo.

Observe además que una declaración de consulta dentro de un elemento <hibernate-mapping> necesita de un nombre único global para la consulta, mientras que una declaración de consulta dentro de un elemento <class> se hace única automáticamente al agregar el nombre completamente calificado de la clase. Por ejemplo, eg.Cat.ByNameAndMaximumWeight.

Las instancias persistentes transaccionales (por ejemplo, los objetos cargados, creados o consultados por la Session) pueden ser manipulados por la aplicación y cualquier cambio al estado persistente será persistido cuando se vacie la Session. Esto se discute más adelante en este capítulo. No hay necesidad de llamar a un método en particular (como update(), que tiene un propósito diferente) para hacer persistentes sus modificaciones. De modo que la forma más directa de actualizar el estado de un objeto es cargarlo con load() y luego manipularlo directamente, mientras la Session está abierta:

DomesticCat cat = (DomesticCat) sess.load( Cat.class, new Long(69) );

cat.setName("PK");
sess.flush();  // changes to cat are automatically detected and persisted

A veces este modelo de programación es ineficiente pues requiere un SELECT de SQL para cargar un objeto y un UPDATE de SQL para hacer persistente su estado actualizado en la misma sesión. Por lo tanto, Hibernate ofrece un enfoque opcional, utilizando instancias separadas.

Muchas aplicaciones necesitan recuperar un objeto en una transacción, enviarla a la capa de UI para su manipulación, y entonces guardar los cambios en una nueva transacción. Las aplicaciones que usan este tipo de enfoque en un entorno de alta concurrencia usualmente utilizan datos versionados para asegurar el aislamiento de la unidad de trabajo "larga".

Hibernate soporta este modelo al proveer re-unión de instancias separadas utilizando los métodos Session.update() o Session.merge():

// in the first session

Cat cat = (Cat) firstSession.load(Cat.class, catId);
Cat potentialMate = new Cat();
firstSession.save(potentialMate);
// in a higher layer of the application
cat.setMate(potentialMate);
// later, in a new session
secondSession.update(cat);  // update cat
secondSession.update(mate); // update mate

Si el Cat con identificador catId ya hubiera sido cargado por secondSession cuando la aplicación intentó volver a unirlo, se habría lanzado una excepción.

Utilice update() si está seguro de que la sesión no tiene una instancia ya persistente con el mismo identificador. Utilice merge() si quiere fusionar sus modificaciones en cualquier momento sin consideración del estado de la sesión. En otras palabras, update() usualmente es el primer método que usted llamaría en una sesión actualizada, asegurando que la re-unión de sus instancias separadas es la primera operación que se ejecuta.

The application should individually update() detached instances that are reachable from the given detached instance only if it wants their state to be updated. This can be automated using transitive persistence. See Sección 11.11, “Persistencia transitiva” for more information.

El método lock() también le permite a una aplicación reasociar un objeto con una sesión nueva. Sin embargo, la instancia separada no puede haber sido modificada.

//just reassociate:

sess.lock(fritz, LockMode.NONE);
//do a version check, then reassociate:
sess.lock(izi, LockMode.READ);
//do a version check, using SELECT ... FOR UPDATE, then reassociate:
sess.lock(pk, LockMode.UPGRADE);

Note que lock() se puede utilizar con varios LockModes. Consulte la documentación de la API y el capítulo sobre el manejo de transacciones para obtener mayor información. La re-unión no es el único caso de uso para lock().

Other models for long units of work are discussed in Sección 13.3, “Control de concurrencia optimista”.

Los usuarios de Hibernate han pedido un método de propósito general que bien guarde una instancia transitoria generando un identificador nuevo, o bien actualice/reúna las instancias separadas asociadas con su identificador actual. El método saveOrUpdate() implementa esta funcionalidad.

// in the first session

Cat cat = (Cat) firstSession.load(Cat.class, catID);
// in a higher tier of the application
Cat mate = new Cat();
cat.setMate(mate);
// later, in a new session
secondSession.saveOrUpdate(cat);   // update existing state (cat has a non-null id)
secondSession.saveOrUpdate(mate);  // save the new instance (mate has a null id)

La utilización y semántica de saveOrUpdate() parece ser confuso para los usuarios nuevos. Primero, en tanto no esté tratando de utilizar instancias de una sesión en otra sesión nueva, no debe necesitar usar update(), saveOrUpdate(), o merge(). Algunas aplicaciones enteras nunca usarán ninguno de estos métodos.

Usualmente update() o saveOrUpdate() se utilizan en el siguiente escenario:

saveOrUpdate() hace lo siguiente:

y merge() es muy diferente:

Session.delete() borrará el estado de un objeto de la base de datos. Sin embargo, su aplicación puede tener todavía una referencia a un objeto borrado. Lo mejor es pensar en delete() al hacer transitoria una instancia persistente.

sess.delete(cat);

Puede borrar objetos en el orden que quiera, sin riesgo de violaciones de restricción de clave foránea. Aún es posible violar una restricción NOT NULL sobre una columna de clave foránea borrando objetos en un orden erróneo, por ejemplo, si borra el padre, pero olvida borrar los hijos.

A veces es útil poder tomar un grafo de la instancias persistentes y hacerlas persistentes en un almacenamiento de datos diferente, sin regenerar los valores identificadores.

//retrieve a cat from one database

Session session1 = factory1.openSession();
Transaction tx1 = session1.beginTransaction();
Cat cat = session1.get(Cat.class, catId);
tx1.commit();
session1.close();
//reconcile with a second database
Session session2 = factory2.openSession();
Transaction tx2 = session2.beginTransaction();
session2.replicate(cat, ReplicationMode.LATEST_VERSION);
tx2.commit();
session2.close();

El ReplicationMode determina cómo replicate() tratará los conflictos con filas existentes en la base de datos:

Los casos de uso para esta funcionalidad incluyen reconciliar datos ingresados en instancias diferentes de bases de datos, actualizar información de configuración del sistema durante actualizaciones de producto, deshacer cambios realizados durante transacciones no-ACID y más.

A veces la Session ejecutará las declaraciones SQL necesarias para sincronizar el estado de la conexión JDBC con el estado de los objetos en la menoria. Este proceso, denominado vaciado (flush), ocurre por defecto en los siguientes puntos:

Las declaraciones SQL se emiten en el siguiente orden:

Una excepción es que los objetos que utilizan generación de ID native se insertan cuando se guardan.

Excepto cuando llama explícitamente a flush(), no hay en absoluto garantías sobre cuándo la Session ejecuta las llamadas JDBC, sólamente sobre el orden en que se ejecutan. Sin embargo, Hibernate garantiza que los métodos Query.list(..) nunca devolverán datos desactualizados o incorrectos.

It is possible to change the default behavior so that flush occurs less frequently. The FlushMode class defines three different modes: only flush at commit time when the Hibernate Transaction API is used, flush automatically using the explained routine, or never flush unless flush() is called explicitly. The last mode is useful for long running units of work, where a Session is kept open and disconnected for a long time (see Sección 13.3.2, “Sesión extendida y versionado automático”).

sess = sf.openSession();

Transaction tx = sess.beginTransaction();
sess.setFlushMode(FlushMode.COMMIT); // allow queries to return stale state
Cat izi = (Cat) sess.load(Cat.class, id);
izi.setName(iznizi);
// might return stale data
sess.find("from Cat as cat left outer join cat.kittens kitten");
// change to izi is not flushed!
...
tx.commit(); // flush occurs
sess.close();

During flush, an exception might occur (e.g. if a DML operation violates a constraint). Since handling exceptions involves some understanding of Hibernate's transactional behavior, we discuss it in Capítulo 13, Transacciones y concurrencia.

Es absolutamente incómodo guardar, borrar, o reunir objetos individuales, especialmente si trata con un grafo de objetos asociados. Un caso común es una relación padre/hijo. Considere el siguiente ejemplo:

Si los hijos en una relación padre/hijo pudieran ser tipificados en valor (por ejemplo, una colección de direcciones o cadenas), sus ciclos de vida dependerían del padre y no se requeriría ninguna otra acción para el tratamiento apropiado en "cascada" de los cambios de estado. Cuando se guarda el padre, los objetos hijo tipificados en valor también se guardan, cuando se borra el padre, se borran los hijos, etc. Esto funciona incluso para operaciones tales como el retiro de un hijo de la colección. Hibernate detectará esto y ya que los objetos tipificados en valor no pueden tener referencias compartidas entonces borrará el hijo de la base de datos.

Ahora considere el mismo escenario con los objetos padre e hijos siendo entidades, no tipos de valor (por ejemplo, categorías e ítems, o gatos padres e hijos). Las entidades tienen su propio ciclo de vida y soportan referencias compartidas. El eliminar una entidad de una colección no significa que se pueda borrar, y no hay por defecto ningún tratamiento en "cascada" del estado de una entidad a otras entidades asociadas. Hibernate no implementa por defecto la persistencia por alcance.

Para cada operación básica de la sesión de Hibernate - incluyendo persist(), merge(), saveOrUpdate(), delete(), lock(), refresh(), evict(), replicate() - existe un estilo de cascada correspondiente. Respectivamente, los estilos de cascada se llaman create, merge, save-update, delete, lock, refresh, evict, replicate. Si quiere que una operación sea tratada en cascada a lo largo de una asociación, debe indicar eso en el documento de mapeo. Por ejemplo:


<one-to-one name="person" cascade="persist"/>

Los estilos de cascada pueden combinarse:


<one-to-one name="person" cascade="persist,delete,lock"/>

Incluso puede utilizar cascade="all" para especificar que todas las operaciones deben ser tratadas en cascada a lo largo de la asociación. La cascade="none" predeterminada especifica que ninguna operación se tratará en cascada.

In case you are using annotatons you probably have noticed the cascade attribute taking an array of CascadeType as a value. The cascade concept in JPA is very is similar to the transitive persistence and cascading of operations as described above, but with slightly different semantics and cascading types:

A special cascade style, delete-orphan, applies only to one-to-many associations, and indicates that the delete() operation should be applied to any child object that is removed from the association. Using annotations there is no CascadeType.DELETE-ORPHAN equivalent. Instead you can use the attribute orphanRemoval as seen in Ejemplo 11.4, “@OneToMany with orphanRemoval”. If an entity is removed from a @OneToMany collection or an associated entity is dereferenced from a @OneToOne association, this associated entity can be marked for deletion if orphanRemoval is set to true.


Recomendaciones:

  • It does not usually make sense to enable cascade on a many-to-one or many-to-many association. In fact the @ManyToOne and @ManyToMany don't even offer a orphanRemoval attribute. Cascading is often useful for one-to-one and one-to-many associations.

  • If the child object's lifespan is bounded by the lifespan of the parent object, make it a life cycle object by specifying cascade="all,delete-orphan"(@OneToMany(cascade=CascadeType.ALL, orphanRemoval=true)).

  • En otro caso, puede que usted no necesite tratamiento en cascada en absoluto. Pero si piensa que va a estar trabajando frecuentemente con padre e hijos juntos en la misma transacción, y quiere ahorrarse algo de escritura en computador, considere el utilizar cascade="persist,merge,save-update".

Mapear una asociación (ya sea una asociación monovaluada, o una colección) con cascade="all" marca la asociación como una relación del estilo padre/hijo en donde guardar/actualizar/borrar (save/update/delete) el padre causa el guardar/actualizar/borrar del hijo o hijos.

Furthermore, a mere reference to a child from a persistent parent will result in save/update of the child. This metaphor is incomplete, however. A child which becomes unreferenced by its parent is not automatically deleted, except in the case of a one-to-many association mapped with cascade="delete-orphan". The precise semantics of cascading operations for a parent/child relationship are as follows:

  • Si un padre pasa a persist(), se pasan todos los hijos a persist()

  • Si un padre pasa a merge(), se pasan todos los hijos a merge()

  • Si se pasa un padre a save(), update() o saveOrUpdate(), todos los hijos pasan a saveOrUpdate()

  • Si un hijo transitorio o separado se vuelve referenciado por un padre persistente, le es pasado a saveOrUpdate()

  • Si se borra un padre, se pasan todos los hijos a delete()

  • Si un hijo deja de ser referenciado por un padre persistente, no ocurre nada especial - la aplicación debe borrar explícitamente el hijo de ser necesario - a menos que cascade="delete-orphan", en cuyo caso se borra el hijo "huérfano".

Finalmente, note que las operaciones en cascadas se pueden aplicar a un grafo de objeto en tiempo de llamada o en tiempo de vaciado. Todas las operaciones, si se encuentran activadas se tratan en cascadas en entidades asociadas alcanzables cuando se ejecuta la operación. Sin embargo, save-upate y delete-orphan son transitivos para todas las entidades asociadas alcanzables durante el vaciado de la Session.

Hibernate requiere de un modelo de meta-nivel muy rico de todas las entidades y tipos de valor. Este modelo puede ser útil para la aplicación misma. Por ejemplo, la aplicación podría utilizar los metadatos de Hibernate para implementar un algoritmo "inteligente" de copia en profundidad que entienda qué objetos deben ser copiados (por ejemplo, tipos de valor mutables) y cuáles no (por ejemplo, tipos de valor inmutables y posiblemente las entidades asociadas).

Hibernate expone los metadatos por medio de las interfaces ClassMetadata y CollectionMetadata y la jerarquía Type. Las instancias de las interfaces de metadatos se pueden obtener de la SessionFactory.

Cat fritz = ......;

ClassMetadata catMeta = sessionfactory.getClassMetadata(Cat.class);
Object[] propertyValues = catMeta.getPropertyValues(fritz);
String[] propertyNames = catMeta.getPropertyNames();
Type[] propertyTypes = catMeta.getPropertyTypes();
// get a Map of all properties which are not collections or associations
Map namedValues = new HashMap();
for ( int i=0; i<propertyNames.length; i++ ) {
    if ( !propertyTypes[i].isEntityType() && !propertyTypes[i].isCollectionType() ) {
        namedValues.put( propertyNames[i], propertyValues[i] );
    }
}