Trabajando con objetos
Hibernate es una solución completa de mapeo objeto/relacional que no sólo
abstrae al desarrollador de los detalles del sistema de manejo de base datos
subyacente, sino que además ofrece manejo de estado de
objetos. Esto es, al contrario del manejo de sentencias
SQL en capas comunes de persistencia JDBC/SQL, una vista de la persistencia
en aplicaciones Java muy natural y orientada a objetos.
En otras palabras, los desarroladores de aplicaciones Hibernate deben siempre
pensar en el estado de sus objetos, y no necesariamente
en la ejecución de sentencias SQL. Esta parte es cuidada por Hibernate y es
sólo relevante para el desarrollador de la aplicación al afinar el rendimiento
del sistema.
Estados de objeto de Hibernate
Hibernate define y soporta los siguientes estados de objeto:
Transitorio - un objeto es transitorio si ha sido
recién instanciado usando el operador new, y no está
asociado a una Session de Hibernate. No tiene una
representación persistente en la base de datos y no se le ha asignado un
valor identificador. Las instancias transitorias serán destruídas por el
recolector de basura si la aplicación no mantiene más una referencia.
Usa la Session de Hibernate para hacer un objeto
persistente (y deja que Hibernate cuide de las sentencias SQL que necesitan
ejecutarse para esta transición).
Persistente - una instancia persistente tiene una
representación en la base de datos y un valor identificador. Puede haber
sido salvado o cargado, sin embargo, está por definición en el ámbito de
una Session. Hibernate detectará cualquier cambio hecho
a un objeto en estado persistentey sincronizará el estado con la base de
datos cuando se complete la unidad de trabajo. Los desarrolladores no ejecutan
sentencias UPDATE manuales, o sentencias DELETE
cuando un objeto debe ser hecho transitorio.
Separado (detached) - una instancia separada es un objeto
que ha sido hecho persistente, pero su Session ha sido cerrada.
La referencia al objeto todavía es válida, por supuesto, y la instancia separada
podría incluso ser modificada en este estado. Una instancia separada puede ser
re-unida a una nueva Session en un punto posterior en el tiempo,
haciéndola persistente de nuevo (con todas las modificaciones). Este aspecto
habilita un modelo de programación para unidades de trabajo de ejecución larga
que requieren tiempo-para-pensar del usuario. Las llamamos transaccciones
de aplicación, es decir, una unidad de trabajo desde el punto de vista
del usuario.
Discutiremos ahora los estados y transiciones de estados (y los métodos de Hibernate que
disparan una transición) en más detalle:
Haciendo los objetos persistentes
Las instancias recién instanciadas de una clase persistente son consideradas
transitorias por Hibernate. Podemos hacer una instancia
transitoria persistente asociándola con una sesión:
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(). Puedes también
usar persist() en vez de save(), con la semántica
definida en el temprano borrador de EJB3.
Alternativamente, puedes asignar el identificador usando una versión sobrecargada
de save().
Si el objeto que haces persistente tiene objetos asociados (por ejemplo,
la colección kittens en el ejemplo anterior), estos
objetos pueden ser hechos persistentes en cualquier orden que quieras
a menos que tengas una restricción NOT NULL sobre una
columna clave foránea. Nunca hay riesgo de violar restricciones de clave
foránea. Sin embargo, podrías violar una restricción NOT NULL
si llamas a save() sobre objetos en orden erróneo.
Usualmente no te preocupas con este detalle, pues muy probablemente usarás
la funcionalidad de persistencia transitiva de Hibernate
para salvar los objetos asociados automáticamente. Entonces, ni siquiera ocurren
violaciones de restricciones NOT NULL - Hibernate cuidará de todo.
La persistencia transitiva se discute más adelante en este capítulo.
Cargando un objeto
Los métodos load() de Session te brindan
una forma de traer una instancia persistente si ya saves su identificador.
load() toma un objeto clase y cargará el estado dentro de
una instancia recién instanciada de esta clase, en estado persistente.
Alternativamente, puedes cargar estado dentro de una instancia dada:
Nota que load() lanzará una excepción irrecuperable si no
hay una fila correspondiente en base de datos. Si la clase es mapeada con un
proxy, load() sólo devuelve un proxy no inicializado y no
llamará realmente a la base de datos hasta que invoques un método del proxy.
Este comportamiento es muy útil si deseas crear una asociación a un objeto
sin cargarlo realmente de la base de datos. Permite además que múltiples
instancias sean cargadas como un lote si se define batch-size
para el mapeo de la clase.
Si no tienes certeza que exista una fila correspondiente, debes usar el
método get(), que llama a la base de datos inmediatamente
y devuelve nulo si no existe una fila correspondiente.
Puedes incluso cargar un objeto usando un SELECT ... FOR UPDATE de SQL,
usando un LockMode. Ver la documentación de la API para más
información.
Ten en cuenta que ninguna instancia asociada o colección contenida es
selecciona FOR UPDATE, a menos que decidas 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,
usando el método refresh(). Esto es útil cuando se usan disparadores de
base de datos para inicializar algunas de las propiedades del objeto.
Una cuestión importante aparece usualmente en este punto: ¿Cuánto carga Hibernate de
la base de datos y cuántos SELECTs de SQL usará? Esto depende de la
estrategia de recuperación y se explica en .
Consultando
Si no sabes los identificadores de los objetos que estás buscando,
necesitas una consulta. Hibernate soporta un lenguaje de consulta
orientado a objetos (HQL) fácil de usar pero potente. Para la creación
de consultas programáticas, Hibernate soporta una funcionalidad sofisticada
de consulta de Criteria y Example (QBC and QBE). También puedes expresar tu
consulta en el SQL nativo de tu base de datos, con soporte opcional de Hibernate
para la conversión del conjunto resultado en objetos.
Ejecutando consultas
Las consultas HQL y SQL nativas son representadas con una instancia de
org.hibernate.Query. Esta interface ofrece métodos para
la ligación de parámetros, manejo del conjunto resultado, y para la
ejecución de la consulta real. Siempre obtienes una Query
usando la Session actual:
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 traídas por una consulta
están en estado persistente. El método uniqueResult()
ofrece un atajo si sabes que tu consulta devolverá sólo un objeto.
Iterando los resultados
Ocasionalmente, podrías ser capaz de lograr mejor rendimiento al ejecutar la consulta
usando el método iterate(). Esto sólo será en el caso que esperes
que las instancias reales de entidad devueltas por la consulta estén ya en la sesión
o caché de segundo nivel. Si todavía no están en caché, iterate()
será más lento que list() y podría requerir muchas llamadas a la
base de datos para una consulta simple, usualmente 1 para la
selección inicial que solamente devuelve identificadores, y n
selecciones adicionales para inicializar las instancias reales.
Consultas que devuelven tuplas
Las consultas de Hibernate a veces devuelven tuplas de objetos, en cuyo caso
cada tupla se devuelve como un array:
Resultados escalares
Las consultas pueden especificar una propiedad de una clase en la cláusula
select. Pueden incluso llamar a funciones de agregación SQL.
Las propiedades o agregaciones son considerados resultados "escalares"
(y no entidades en estado persistente).
Ligación de parámetros
Se proveen métodos en Query para ligar valores a
parámetros con nombre o parámetros ? de estilo JDBC.
Al contrario de JDBC, Hibernate numera los parámetros desde cero.
Los parámetros con nombre son identificadores de la forma :name
en la cadena de la consulta. Las ventajas de los parámetros con nombre son:
los parámetros con nombre son insensibles al orden en que aparecen
en la cadena de consulta
pueden aparecer múltiples veces en la misma consulta
son auto-documentados
Iteración scrollable
Si tu driver JDBC soporta ResultSets scrollables, la
interface Query puede ser usada para obtener un objeto
ScrollableResults, que permite una navegación flexible
de los resultados de consulta.
i++ ) && cats.next() ) pageOfCats.add( cats.get(1) );
}
cats.close()]]>
Nota que se requiere una conexión de base de datos abierta (y cursor) para esta
funcionalidad, usa setMaxResult()/setFirstResult()
si necesitas la funcionalidad de paginación fuera de línea.
Externalizando consultas con nombre
Puedes además definir consultas con nombre en el documento de mapeo.
(Recuerda usar una sección CDATA si tu consulta
contiene caracteres que puedan ser interpretados como etiquetado.)
?
] ]>]]>
La ligación de parámetros y ejecución se hace programáticamente:
Nota que el código real del programa es independiente del lenguaje de consulta
usado; puedes además definir consultas SQL nativas en metadatos, o migrar
consultas existentes a Hibernate colocándolas en ficheros de mapeo.
Filtrando colecciones
Un filtro de colección es un tipo especial de consulta que puede ser
aplicado a una colección persistente o array. La cadena de consulta puede referirse a
this, significando el elemento de colección actual.
La colección devuelta es considerada un bag, y es una copia de la colección
dada. La colección original no es modificada (esto es contrario a la implicación
del nombre "filtro", pero consistente con el comportamiento esperado).
Observa que los filtros no requieren una cláusula from (aunque pueden
tener uno si se requiere). Los filtros no están limitados a devolver los elementos de
colección por sí mismos.
Incluso una consulta de filtro vacío es útil, por ejemplo, para cargar un
subconjunto de elementos en una colección enorme:
Consultas de criterios
HQL es extremadamente potente pero algunos desarrolladores prefieren construir
consultas dinámicamente usando una API orientada a objetos, en vez construir
cadenas de consulta. Hibernate provee una API intuitiva de consulta Criteria
para estos casos:
Las APIs de Criteria y la asociada Example
son discutidas en más detalle en .
Consultas en SQL nativo
Puedes expresar una consulta en SQL, usando createSQLQuery() y
dejando que Hibernate cuide del mapeo de los conjuntos resultado a objetos.
Nota que puedes llamar en cualquier momento a session.connection() y
usar la Connection JDBC directamente. Si eliges usar la API de
Hibernate, debes encerrar los alias de SQL entre llaves:
Las consultas SQL pueden contener parámetros con nombre y posicionales, al igual que
las consultas de Hibernate. Puede encontrarse más información sobre consultas en SQL
nativo en .
Modificando objetos persistentes
Las instancias persistentes transaccionales (es decir, objetos cargados,
creados o consultados por la Session) pueden ser manipulados por la
aplicación y cualquier cambio al estado persistente será persistido cuando la Session
sea limpiada (flushed) (discutido más adelante en este capítulo). No hay
necesidad de llamar un método en particular (como update(), que tiene un
propósito diferente) para hacer persistentes tus modificaciones. De modo que la forma más
directa de actualizar el estado de un objeto es cargarlo con load(),
y entonces manipularlo directamente, mientras la Session está abierta:
A veces este modelo de programación es ineficiente pues podría requerir una
SELECT de SQL (para cargar un objeto) y un UPDATE
de SQL (para hacer persistentes sus datos actualizados) en la misma sesión. Por lo tanto,
Hibernate ofrece un enfoque alternativo, usando instancias separadas (detached).
Nota que Hibernate no ofreve su propia API para ejecución directa de
sentencias UPDATE o DELETE. Hibernate es un
servicio de gestión de estado, no tienes que pensar en
sentencias para usarlo. JDBC es una API perfecta para ejecutar
sentencias SQL; puedes obtener una Connection JDBC en cualquier
momento llamando a session.connection(). Además, la noción de
operaciones masivas entra en conflicto con el mapeo objeto/relacional en aplicaciones
en línea orientadas al procesamiento de transacciones. Versiones futuras de Hibernate
pueden, sin embargo, proveer funciones de operación masiva especiales. Ver
por algunos trucos de operación en lote (batch) posibles.
Modificando objetos separados
Muchas aplicaciones necesitan recuperar un objeto en una transacción, enviarla
a la capa de UI para su manipulación, y entonces salvar los cambios en una nueva
transacción. Las aplicaciones que usan este tipo de enfoque en un entorno de
alta concurrencia usualmente usan datos versionados para asegurar el aislamiento
de la unidad de trabajo "larga".
Hibernate soporta este modelo al proveer re-unión de instancias separadas usando
los métodos Session.update() o Session.merge():
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.
Usa update() si no estás seguro que la sesión tenga
una instancia ya persistente con el mismo identificador, y merge()
si quieres fusionar tus modificaciones en cualquier momento sin consideración del
estado de la sesión. En otras palabras, update() es usualmente
el primer método que llamarías en una sesión fresca, asegurando que la re-unión de
tus instancias separadas es la primera operación que se ejecuta.
La aplicación debe actualizar individualmente las instancias separadas alcanzables
por la instancia separada dada llamando a update(), si y
sólo si quiere que sus estados sean también actualizados.
Esto puede, por supuesto, ser automatizado usando persistencia transitiva,
ver .
El método lock() también permite a una aplicación reasociar
un objeto con una sesión nueva. Sin embargo, la instancia separada no puede
haber sido modificada!
Nota que lock() puede ser usado con varios LockModes,
ver la documentación de la API y el capítulo sobre manejo de transacciones para más
información. La re-unión no es el único caso de uso para lock().
Se discuten otros modelos para unidades de trabajo largas en .
Detección automática de estado
Los usuarios de Hibernate han pedido un método de propósito general que bien
salve 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.
El uso y semántica de saveOrUpdate() parece ser confuso para
usuarios nuevos. Primeramente, en tanto no estés intentando usar instancias de una
sesión en otra sesión nueva, no debes necesitar usar update(),
saveOrUpdate(), o merge(). Algunas aplicaciones
enteras nunca usarán ninguno de estos métodos.
Usualmente update() o saveOrUpdate() se usan en
el siguiente escenario:
la aplicación carga un objeto en la primera sesión
el objeto es pasado a la capa de UI
se hacen algunas modificaciones al objeto
el objeto se pasa abajo de regreso a la capa de negocio
la aplicación hace estas modificaciones persistentes llamando
a update() en una segunda sesión
saveOrUpdate() hace lo siguiente:
si el objeto ya es persistente en esta sesión, no hace nada
si otro objeto asociado con la sesión tiene el mismo identificador,
lanza una excepción
si el objeto no tiene ninguna propiedad identificadora, lo salva llamando a
save()
si el identificador del objeto tiene el valor asignado a un objeto recién
instanciado, lo salva llamando a save()
si el objeto está versionado (por un <version> o
<timestamp>), y el valor de la propiedad de versión
es el mismo valor asignado a una objeto recién instanciado, lo salva llamando
a save()
en cualquier otro caso se actualiza el objeto llamando a update()
y merge() es muy diferente:
si existe una instancia persistente con el mismo identificador asignado actualmente con la
sesión, copia el estado del objeto dado en la instancia persistente
si no existe ninguna instancia persistente actualmente asociada a la sesión,
intente cargarla de la base de datos, o crear una nueva instancia persistente
la instancia persistente es devuelta
la instancia dada no resulta ser asociada a la sesión, permanece separada
Borrando objetos persistentes
Session.delete() quitará el estado de un objeto de la base de datos.
Por supuesto, tu aplicación podría tener aún una referencia a un objeto borrado. Lo mejor
es pensar en delete() como hacer transitoria una instancia persistente.
Puedes borrar los objetos en el orden que gustes, sin riesgo de violaciones
de restricción de clave foránea. Aún es posible violar una restricción
NOT NULL sobre una columna clave foránea borrando objetos
en un orden erróneo, por ejemplo, si borras el padre, pero olvidas borrar los
hijos.
Replicando objetos entre dos almacénes de datos diferentes
Es ocasionalmente útil ser capaz de tomar un grafo de instancias persistentes
y hacerlas persistentes en un almacén de datos diferente, sin regenerar los valores
identificadores.
El ReplicationMode determina cómo replicate()
tratará los conflictos con filas existentes en la base de datos.
ReplicationMode.IGNORE - ignora el objeto cuando existe una fila
de base de datos con el mismo identificador
ReplicationMode.OVERWRITE - sobrescribe cualquier fila de base de
datos existente con el mismo identificador
ReplicationMode.EXCEPTION - lanza una excepción si existe una fila
de base de datos con el mismo identificador
ReplicationMode.LATEST_VERSION - sobrescribe la fila si su número
de versión es anterior al número de versión del objeto, o en caso contrario ignora el objeto
Los casos de uso para esta funcionalidad incluyen reconciliar datos ingresados en
instancias diferentes de bases de datos, actualizar información de configuración de
sistema durante actualizaciones de producto, deshacer cambios producidos durante
transacciones no-ACID y más.
Limpiando (flushing) la sesión
Cada tanto, la Session ejecutará las sentencias SQL necesarias para
sincronizar el estado de la conexión JDBC con el estado de los objetos mantenidos en menoria.
Este proceso, limpieza (flush), ocurre por defecto en los siguientes
puntos
antes de algunas ejecuciones de consulta
desde org.hibernate.Transaction.commit()
desde Session.flush()
Las sentencias SQL son liberadas en el siguiente orden
todas las inserciones de entidades, en el mismo orden que los objetos
correspondientes fueron salvados usando Session.save()
todas las actualizaciones de entidades
todas los borrados de colecciones
todos los borrados, actualizaciones e inserciones de elementos de colección
todas las inserciones de colecciones
todos los borrados de entidades, en el mismo orden que los objetos
correspondientes fueron borrados usando Session.delete()
(Una excepción es que los objetos que usan generación de ID native
se insertan cuando son salvados.)
Excepto cuando llamas explícitamente a flush(), no hay en absoluto
garantías sobre cuándo la Session ejecuta las
llamadas JDBC. sólo sobre el orden en que son ejecutadas. Sin embargo,
Hibernate garantiza que los métodos Query.list(..) nunca devolverán datos
añejos o erróneos.
Es posible cambiar el comportamiento por defecto de modo que la limpieza (flush)
ocurra menos frecuentemente. La clase FlushMode tres modos
diferentes: sólo en tiempo de compromiso (y sólo cuando se use la API de
Transaction de Hibernate), limpieza automática usando la rutina
explicada, o nunca limpiar a menos que se llame a flush()
explícitamente. El último modo es útil para unidades de trabajo largas, donde una
Session se mantiene abierta y desconectada por largo tiempo
(ver ).
Durante la limpieza, puede ocurrir una excepción (por ejemplo, si una operación DML
violase una restricción). Ya que el manejo de excepciones implica alguna comprensión
del comportamiento transaccional de Hibernate, lo discutimos en .
Persistencia transitiva
Es absolutamente incómodo dalvar, borrar, o reunir objetos individuales,
especialmente si tratas con un grafo de objetos asociados. Un caso común es
una relación padre/hijo. Considera 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 se requeriría ninguna otra acción para el tratamiento
en "cascada" de cambios de estado. Cuando el padre es salvado, los objetos hijo
tipificados en valor son salvados también, cuando se borra el padre, se borran
los hijos, etc. Esto funciona incluso para operaciones 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, borrará el hijo de la base
de datos.
Ahora considera el mismo escenario con los objetos padre e hijos siendo entidades,
no tipos de valor (por ejemplo, categorías e ítems, o gatos padre e hijos).
Las entidades tienen su propio ciclo de vida, soportan referencias compartidas
(de modo que quitar una entidad de una colección no significa que sea borrada),
y no hay por defecto ningún tratamiento en "cascada" de estado de una entidad
a otras entidades asociadas. Hibernate no implementa persistencia por
alcance.
Para cada operación básica de la sesión de Hibernate - incluyendo persist(), merge(),
saveOrUpdate(), delete(), lock(), refresh(), evict(), replicate() - hay un estilo
de cascada correspondiente. Respectivamente, los estilos de cascada se llaman create,
merge, save-update, delete, lock, refresh, evict, replicate. Si quieres que una
operación sea tratada en cascada a lo largo de una asociación, debes indicar eso en el
documento de mapeo. Por ejemplo:
]]>
Los estilos de cascada pueden combinarse:
]]>
Puedes incluso usar cascade="all" para especificar que todas
las operaciones deben ser tratadas en cascada a lo largo de la asociación. El por defecto
cascade="none" especifica que ninguna operación será tratada en cascada.
Un estilo de cascada especial, delete-orphan, se aplica sólo a
asociaciones uno-a-muchos, e indica que la operación delete() debe
aplicarse a cualquier objeto hijo que sea quitado de la asociación.
Recomendaciones:
Usualmente no tiene sentido habilitar el tratamiento en cascada a una asociación
<many-to-one> o <many-to-many>.
El tratamiento en cascada es frecuentemente útil para las asociaciones
<one-to-one> y <one-to-many>.
associations.
Si la esperanza de vida de los objetos hijos está ligada a la eesperanza de
vida del objeto padre, házlo un objeto de ciclo de vida
especificando cascade="all,delete-orphan".
En otro caso, puede que no necesites tratamiento en cascada en absoluto. Pero
si piensas que estarás trabajando frecuentemente con padre e hijos juntos en la
misma transacción, y quieres ahorrarte algo de tipeo, considera usar
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 donde save/update/delete en el padre resulta
en save/update/delete del hijo o hijos.
Además, una mera referencia a un hijo desde un padre persistente resultará en
un save/update del hijo. Esta metáfora está incompleta, sin embargo. Un hijo
que deje de ser referenciado por su padre no es borrado
automáticamente, excepto en el caso de una asociación <one-to-many>
mapeada con cascade="delete-orphan". La semántica precisa de
las operaciones en cascada para una relación padre/hijo es:
Si un padre le es pasado a persist(), todos los hijos le son
pasados a persist()
Si un padre le es pasado a merge(), todos los hijos le son
pasados a merge()
Si un padre le es pasado a save(), update() o
saveOrUpdate(), todos los hijos le son pasados a saveOrUpdate()
Si un hijo transitorio o separado se vuelve referenciado por un padre
persistente, le es pasado a saveOrUpdate()
Si un padre es borrado, todos los hijos le son pasados 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 el hijo
"huérfano" es borrado.
Usando metadatos
Hibernate requiere de un modelo de meta-nivel muy rico de todas las entidades y tipos de valor.
De vez en cuando, este modelo es muy útil para la aplicación misma. Por ejemplo, la aplicación
podría usar los metadatos de Hibernate para implementar un algoritmo "inteligente" de copia
en profundidad que entienda qué objetos deben ser copiados (por ejemplo, tipo de valor mutables)
y cuáles no (por ejemplo, tipos de valor inmutables y, posiblemente, entidades asociadas).
Hibernate expone los metadatos vía las interfaces ClassMetadata y
CollectionMetadata y la jerarquía Type. Las instancias
de las interfaces de metadatos pueden obtenerse de SessionFactory.