Transacciones y Concurrencia
El punto más importante sobre Hibernate y el control de concurrencia es que muy fácil
de comprender. Hibernate usa directamente conexiones JDBC y recursos JTA sin agregar
ningún comportamiento de bloqueo adicional. Recomendamos altamente que gastes algo de
tiempo con la especificación de JDBC, ANSI, y el aislamiento de transacciones de tu sistema
de gestión de base de datos. Hibernate sólo añade versionado automático pero no bloquea
objetos en memoria ni cambia el nivel de aislamiento de tus transacciones de base de datos.
Básicamente, usa Hibernate como usarías JDBC directo (o JTA/CMT) con tus recursos de base de
datos.
Sin embargo, además del versionado automático, Hibernate ofrece una API (menor) para
bloqueo pesimista de filas, usando la sintáxis SELECT FOR UPDATE.
Esta API se discute más adelante en este capítulo:
Comenzamos la discusión del control de concurrencia en Hibernate con la granularidad
de Configuration, SessionFactory, y
Session, así como la base de datos y las transacciones de aplicación
largas.
Ámbitos de sesión y de transacción
Una SessionFactory es un objeto seguro entre hebras caro-de-crear
pensado para ser compartido por todas las hebras de la aplicación. Es creado una sola vez,
usualmente en el arranque de la aplicación, a partir de una instancia de Configuration.
Una Session es un objeto barato, inseguro entre hebras que debe
ser usado una sola vez, para un solo proceso de negocio, una sola unidad de trabajo,
y luego descartado. Una Session no obtendrá una Connection
JDBC (o un Datasource) a menos que sea necesario, de modo que puedas
abrir y cerrar seguramente una Session incluso si no estás seguro
que se necesitará acceso a los datos para servir una petición en particular. (Esto se
vuelve importante en cuanto estés implementando alguno de los siguientes patrones usando
intercepción de peticiones).
Para completar este cuadro tienes que pensar también en las transacciones de base de
datos. Una transacción de base de datos tiene que ser tan corta como sea posible, para
reducir la contención de bloqueos en la base de datos. Las transacciones largas de base de
datos prevendrán a tu aplicación de escalar a una carga altamente concurrente.
¿Qué es el ámbito de una unidad de trabajo? ¿Puede una sola Session de Hibernate
extenderse a través de varias transacciones de base de datos o es ésta una relación uno-a-uno
de ámbitos? ¿Cuándo debes abrir y cerrar una Session y cómo demarcas los
límites de la transacción de base de datos?
Unidad de trabajo
Primero, no uses el antipatrón sesión-por-operación, esto es,
¡no abras y cierres una Session para cada simple llamada a la base
de datos en una sola hebra! Por supuesto, lo mismo es verdad para transacciones de base de
datos. Las llamadas a base de datos en una aplicación se hacen usando una secuencia
prevista, que están agrupadas dentro de unidades de trabajo atómicas. (Nota que esto
también significa que el auto-commit después de cada una de las sentencias SQL es inútil
en una aplicación, este modo está pensado para trabajo ad-hoc de consola SQL.
Hibernate deshabilita, o espera que el servidor de aplicaciones lo haga, el modo
auto-commit inmediatamente.)
El patrón más común en una aplicación mutiusuario cliente/servidor es
sesión-por-petición. En este modelo, una petición del cliente
es enviada al servidor (en donde se ejecuta la capa de persistencia de Hibernate),
se abre una nueva Session de Hibernate, y todas las operaciones
de base de datos se ejecutan en esta unidad de trabajo. Una vez completado el trabajo
(y se ha preparado la respuesta para el cliente) la sesión es limpiada y cerrada.
Podrías usar una sola transacción de base de datos para servir a petición del cliente,
comenzándola y comprometiéndola cuando abres y cierras la Session.
La relación entre las dos es uno-a-uno y este modelo es a la medida perfecta de muchas
aplicaciones.
El desafío yace en la implementación: no sólo tienen que comenzarse y terminarse correctamente
la Session y la transacción, sino que además tienen que estar accesibles
para las operaciones de acceso a datos. La demarcación de una unidad de trabajo se implementa
idealmente usando un interceptor que se ejecuta cuando una petición llama al servidor y anter que
la respuesta sea enviada (es decir, un ServletFilter). Recomendamos ligar la
Session a la hebra que atiende la petición, usando una variable
ThreadLocal. Esto permite un fácil acceso (como acceder a una variable static)
en tódo el código que se ejecuta en esta hebra. Dependiendo del mecanismo de demarcación de
transacciones de base de datos que elijas, podrías mantener también el contexto de la transacción
en una variable ThreadLocal. Los patrones de implementación para esto son
conocidos como Sesión Local de Hebra (ThreadLocal Session) y
Sesión Abierta en Vista (Open Session in View). Puedes extender fácilmente
la clase de ayuda HibernateUtil mostrada anteriormente para encontrar
una forma de implementar un interceptor e instalarlo en tu entorno. Ver el sitio web de Hibernate
para consejos y ejemplos.
Transacciones de aplicación
El patrón sesión-por-petición no es el único concepto útil que puedes usar para diseñar unidades
de trabajo. Muchos procesos de negocio requiere una serie completa de interacciones con el
usuario intercaladas con accesos a base de datos. En aplicaciones web y de empresa no es aceptable
que una transacción de base de datos se extienda a través de la interacción de un usuario.
Considera el siguiente ejemplo:
Se abre la primera pantalla de un diálogo, los datos vistos por el usuario han sido
cargados en una Session y transacción de base de datos particular.
El usuario es libre de modificar los objetos.
El usuario hace click en "Salvar" después de 5 minutos y espera que sus modificaciones
sean hechas persistentes. También espera que él sea la única persona editando esta
información y que no puede ocurrir ninguna modificación en conflicto.
Llamamos a esto unidad de trabajo, desde el punto de vista del usuario, una larga
transacción de aplicación ejecutándose. Hay muchas formas en
que puedes implementar esto en tu aplicación.
Una primera implementación ingenua podría mantener abierta la Session
y la transacción de base de datos durante el tiempo de pensar del usuario, con bloqueos
tomados en la base de datos para prevenir la modificación concurrente, y para garantizar
aislamiento y atomicidad. Esto es, por supuesto, un antipatrón, ya que la contención de
bloqueo no permitiría a la aplicación escalar con el número de usuarios concurrentes.
Claramente, tenemos que usar muchas transacciones de base de datos para implementar la transacción
de aplicación. En este caso, mantener el aislamiento de los procesos de negocio se vuelve una
responsabilidad parcial de la capa de aplicación. Una sola transacción de aplicación usualmente
abarca varias transacciones de base de datos. Será atómica si sólo una de estas transacciones de
base de datos (la última) almacena los datos actualizados, todas las otras simplemente leen datos
(por ejemplo, en un diálogo estilo-asistente abarcando muchos ciclos petición/respuesta).
Esto es más fácil de implementar de lo que suena, especialmente si usas las funcionalidades de
Hibernate:
Versionado Automático - Hibernate puede llevar un control automático de
concurrencia optimista por ti, puede detectar automáticamente si una modificación concurrente
ha ocurrido durante el tiempo de pensar del usuario.
Objetos Separados - Si decides usar el ya discutido patrón
de sesión-por-petición, todas las instancias cargadas estarán
en estado separado durante el tiempo de pensar del usuario. Hibernate te permite
volver a unir los objetos y hacer persistentes las modificaciones. El patrón se
llama sesión-por-petición-con-objetos-separados. Se usa
versionado automático para aislar las modificaciones concurrentes.
Sesión Larga - La Session de Hibernate puede ser
desconectada de la conexión JDBC subyacente después que se haya sido comprometida la
transacción de base de datos, y reconectada cuando ocurra una nueva petición del cliente.
Este patrón es conocido como sesión-por-transacción-de-aplicación
y hace la re-unión innecesaria. Para aislar las modificaciones concurrentes se usa el
versionado automático.
Tanto sesión-por-petición-con-objetos-separados como
sesión-por-transacción-de-aplicación, ambas tienen
ventajas y desventajas, las discutimos más adelante en este capítulo en el contexto
del control optimista de concurrencia.
Considerando la identidad del objeto
Una aplicación puede acceder concurrentemente a el mismo estado persistente en dos
Sessions diferentes. Sin embargo, una instancia de una clase
persistente nunca se comparte entre dos instancias de Session.
Por lo tanto existen dos nociones diferentes de identidad:
Identidad de Base de Datos
foo.getId().equals( bar.getId() )
Identidad JVM
foo==bar
Entonces para objetos unidos a una Session en particular
(es decir en el ámbito de una Session) las dos nociones son equivalentes, y
la identidad JVM para la identidad de base de datos está garantizada por Hibernate. Sin embargo,
mientras la aplicación acceda concurrentemente al "mismo" (identidad persistente) objeto de negocio
en dos sesiones diferentes, las dos instancias serán realmente "diferentes" (identidad JVM).
Los conflictos se resuelven (con versionado automático) en tiempo de limpieza (flush) usando un
enfoque optimista.
Este enfoque deja que Hibernate y la base de datos se preocupen sobre la concurrencia. Además
provee la mejor escalabilidad, ya que garantizando la identidad un unidades de trabajo monohebra
no se necesitan bloqueos caros u otros medios de sincronización. La aplicación nunca necesita
sincronizar sobre ningún objeto de negocio, siempre que se apegue a una sola hebra por
Session. Dentro de una Session la aplicación puede usar
con seguridad == para comparar objetos.
Sin embargo, una aplicación que usa == fuera de una Session,
podría ver resultados inesperados. Esto podría ocurrir incluso en sitios algo inesperados,
por ejemplo, si pones dos instancias separadas dentro del mismo Set.
Ambas podrían tener la misma identidad de base de datos (es decir, representar la misma fila),
pero la identidad JVM, por definición, no está garantizada para las instancias en estado separado.
El desarrollador tiene que sobrescribir los métodos equals() y
hashCode() en las clases persistentes e implementar su propia noción de igualdad
de objetos. Hay una advertencia: Nunca uses el identificador de base de datos para implementar
la igualdad, usa una clave de negocio, una combinación de atributos únicos, usualmente inmutables.
El identificador de base de datos cambiará si un objeto transitorio es hecho persistente.
Si la instancia transitoria (usualmente junta a instancias separadas) es mantenida en un
Set, cambiar el código hash rompe el contrato del Set.
Los atributos para las claves de negocio no tienen que ser tan estables como las claves primarias
de base de datos, sólo tienes que garantizar estabilidad en tanto los objetos estén en el mismo
Set. Mira el sitio web de Hibernate para una discusión más cuidadosa de este
tema. Nota también que éste no es un tema de Hibernate, sino simplemente cómo la identidad y la igualdad
de los objetos Java tiene que ser implementada.
Temas comunes
Nunca uses los antipatrones sesión-por-sesión-de-usuario o
sesión-por-aplicación (por supuesto, hay raras excepciones a esta
regla). Nota que algunis de los siguientes temas podrían también aparecer con los patrones
recomendados. Asegúrate que entiendes las implicaciones antes de tomar una decisión de
diseño:
Una Session no es segura entre hebras. Las cosas que se suponen
que funcionan concurrentemente, como peticiones HTTP, beans de sesión, o workers de
Swing, provocarán condiciones de competencia si una instancia de Session
fuese compartida. Si guardas tu Session de Hibernate en tu
HttpSession (discutido más adelante), debes considerar sincronizar
el acceso a tu sesión HTTP. De otro modo, un usuario que hace click lo suficientemente
rápido puede llegar a usar la misma Session en dos hebras ejecutándose
concurrentemente.
Una excepción lanzada por Hibernate significa que tienes que deshacer (rollback) tu
transacción de base de datos y cerrar la Session inmediatamente
(discutido en más detalle luego). Si tu Session está ligada a la
aplicación, tienes que parar la aplicación. Deshacer (rollback) la transacción de base
de datos no pone a tus objetos de vuelta al estado en que estaban al comienzo de la
transacción. Esto significa que el estado de la base de datos y los objetos de negocio
quedan fuera de sincronía. Usualmente esto no es un problema, pues las excepciones no
son recuperables y tienes que volver a comenzar después del rollback de todos modos.
La Session pone en caché todo objeto que esté en estado persistente
(vigilado y chequeado por estado sucio por Hibernate). Esto significa que crece sin
fin hasta que obtienes una OutOfMemoryException, si la mantienes abierta por un largo
tiempo o simplemente cargas demasiados datos. Una solución para esto es llamar a
clear() y evict() para gestionar el caché de la
Session, pero probalemente debas considerar un procedimiento almacenado
si necesitas operaciones de datos masivas. Se muestran algunas soluciones en
. Mantener una Session abierta por la duración
de una sesión de usuario significa también una alta probabilidad de datos añejos.
Demarcación de la transacción de base de datos
Los límites de las transacciones de base de datos (o sistema) son siempre necesarios. Ninguna comunicación
con la base de datos puede darse fuera de una transacción de base de datos (esto parece confundir muchos
desarrolladores acostumbrados al modo auto-commit). Siempre usa límites de transacción claros, incluso
para las operaciones de sólo lectura. Dependiendo del nivel de aislamiento y las capacidades de base de
datos, esto podría o no ser requerido, pero no hay un merma si siempre demarcas explícitamente
las transacciones.
Una aplicación Hibernate puede ejecutarse en entornos no manejados (es decir, como independiente,
Web simple, o aplicaciones Swing) y entornos manejados J2EE. En un entorno no manejado, Hibernate es
usualmente responsable de su propio pool de conexiones de base de datos. El desarrollador de aplicaciones
tiene que establecer manualmente los límites de transacción, en otras palabras, hacer begin, commit, o
rollback las transacciones de base de datos por sí mismo. Un entorno manejado usualmente provee transacciones
gestionadas por contenedor, con el ensamble de transacción definido declarativamente en descriptores de
despliegue de beans de sesión EJB, por ejemplo. La demarcación programática de transacciones no es más
necesario, incluso limpiar (flush) la Session es hecho automáticamente.
Sin embargo, frecuentemente es deseable mantener portable tu capa de persistencia. Hibernate ofrece
una API de envoltura llamada Transaction que se traduce al sistema de transacciones
nativo de tu entorno de despliegue. Esta API es realmente opcional, pero recomendamos fuertemente su uso
salvo que estés en un bean de sesión CMT.
Usualmente, finalizar una Session implica cuatro fases distintas:
limpiar (flush) la sesión
comprometer la transacción
cerrar la sesión
manejar excepciones
Limpiar la sesión ha sido discutido anteriormente, tendremos ahora una mirada más de cerca
a la demarcación de transacciones y manejo de excepciones en sendos entornos manejado y no manejados.
Entorno no manejado
Si una capa de persistencia Hibernate se ejecuta en un entorno no manejado, las conexiones
de base de datos son manejadas usualmente por el mecanismo de pooling de Hibernate. El idioma
manejo de sesión/transacción se ve así:
No tienes que limpiar con flush() la Session explícitamente -
la llamada a commit() automáticamente dispara la sincronización.
Una llamada a close() marca el fin de una sesión. La principal implicación
de close() es que la conexión JDBC será abandonada por la sesión.
Este código Java es portable y se ejecuta tanto en entornos no manejados como en entornos JTA.
Muy probablemente nunca veas este idioma en código de negocio en una aplicación normal;
las excepciones fatales (sistema) deben siempre ser capturadas en la "cima". En otras palabras,
el código que ejecuta las llamadas de Hibernate (en la capa de persistencia) y el código que
maneja RuntimeException (y usualmente sólo puede limpiar y salir) están en
capas diferentes. Esto puede ser un desafío de diseñarlo tú mismo y debes usar los servicios
de contenedor J2EE/EJB en cuanto estuviesen disponibles. El manejo de excepciones se dicute
más adelante en este capítulo.
Nota que debes seleccionar org.hibernate.transaction.JDBCTransactionFactory
(que es el por defecto).
Usando JTA
Si tu capa de persistencia se ejecuta en un servidor de aplicaciones (por ejemplo, detrás
de beans de sesión EJB), cada conexión de datasource obtenida por Hibernate será parte
automáticamente de la transacción JTA global. Hibernate ofrece dos estrategias para esta
integración.
Si usas transacciones gestionadas-por-bean (BMT) Hibernate le dirá al servidor de aplicaciones
que comience y finalice una transacción BMT si usas la API de Transaction.
De modo que, el código de gestión de la transacción es idéntico al de un entorno no manejado.
Con CMT, la demarcación de la transacción se hace en descriptores de despliegue de beans de sesión,
no programáticamente. Si no quieres limpiar (flush) y cerrar manualmente la Session
por ti mismo, solamente establece hibernate.transaction.flush_before_completion a
true, hibernate.connection.release_mode a
after_statement o auto y
hibernate.transaction.auto_close_session a true. Hibernate
limpiará y cerrará entonces automáticamente la Session para ti. Lo único que resta
es deshacer (rollback) la transacción cuando ocurra una excepción. Afortunadamente, en un bean CMT,
incluso esto ocurre automáticamente, ya que una RuntimeException no manejada
disparada por un método de un bean de sesión le dice al contenedor que ponga a deshacer la transacción
global. Esto significa que, en CMT, no necesitas usar en absoluto la API de
Transaction de Hibernate.
Nota que debes elegir org.hibernate.transaction.JTATransactionFactory en un
bean de sesión BMT, y org.hibernate.transaction.CMTTransactionFactory en un
bean de sesión CMT, cuando configures la fábrica de transacciones de Hibernate. Recuerda además
establecer org.hibernate.transaction.manager_lookup_class.
Si trabajas en un entorno CMT, y usas limpieza (flushing) y cierre automáticos de la sesión,
podrías querer también usar la misma sesión en diferentes partes de tu código. Típicamente,
en un entorno no manejado, usarías una variable ThreadLocal para tener la sesión,
pero una sola petición de EJB puede ejecutarse en diferentes hebras (por ejemplo, un bean de sesión
llamando a otro bean de sesión). Si no quieres molestarte en pasar tu Session
por alrededor, la SessionFactory provee el método
getCurrentSession(), que devuelve una sesión que está pegada al contexto de
transacción JTA. ¡Esta es la forma más fácil de integrar Hibernate en una aplicación!
La sesión "actual" siempre tiene habilitados limpieza, cierre y liberación de conexión automáticos
(sin importar la configuración de las propiedades anteriores). Nuestra idioma de gestión de
sesión/transacción se reduce a:
En otras palabras, todo lo que tienes que hacer en un entorno manejado, es llamar a
SessionFactory.getCurrentSession(), hacer tu trabajo de acceso a datos,
y dejar el resto al contenedor. Los límites de transacción se establecen declarativamente
en los descriptores de despliegue de tu bean de sesión. El ciclo de vida de la sesión es
manejado completamente por Hibernate.
Existe una advertencia al uso del modo de liberación de conexión after_statement.
Debido a una limitación tonta de la especificación de JTA, no es posible para Hibernate
limpiar automáticamente ningún ScrollableResults no cerrado ni
instancias de Iterator devueltas por scroll() o
iterate(). Debes liberar el cursor de base de datos
subyacente llamando a ScrollableResults.close() o
Hibernate.close(Iterator) explícitamente desde un bloque finally.
(Por supuesto, la mayoría de las aplicaciones pueden evitarlo fácilmente no usando en absoluto ningún
scroll() o iterate() desde el código CMT.)
Manejo de excepciones
Si la Session lanza una excepción (incluyendo cualquier
SQLException), debes inmediatamente deshacer (rollback) la
transacción de base de datos, llamar a Session.close() y
descartar la instancia de Session. Ciertos métodos de
Session no dejarán la sesión en un
estado consistente. Ninguna excepción lanzada por Hibernate puede ser tratada
como recuperable. Asegúrate que la Session sea cerrada llamando
a close() en un bloque finally.
La HibernateException, que envuelve la mayoría de los errores que
pueden ocurrir en la capa de persistencia de Hibernate, en una excepción no chequeada
(no lo era en versiones anteriores de Hibernate). En nuestra opinión, no debemos forzar
al desarrollador de aplicaciones a capturar una excepción irrecuperable en una capa baja.
En la mayoría de los sistemas, las excepciones no chequeadas y fatales son manejadas
en uno de los primeros cuadros de la pila de llamadas a métodos (es decir, en las capas
más altas) y se presenta un mensaje de error al usuario de la aplicación (o se toma alguna
otra acción apropiada). Nota que Hibernate podría también lanzar otras excepciones no chequeadas
que no sean una HibernateException. Una vez más, no son recuperables y debe
tomarse una acción apropiada.
Hibernate envuelve SQLExceptions lanzadas mientras se interactúa con la base
de datos en una JDBCException. De hecho, Hibernate intentará convertir la excepción
en una subclase de JDBCException más significativa. La SQLException
está siempre disponible vía JDBCException.getCause(). Hibernate convierte la
SQLException en una subclase de JDBCException apropiada usando
el SQLExceptionConverter adjunto a la SessionFactory. Por defecto,
el SQLExceptionConverter está definido para el dialecto configurado; sin embargo,
es también posible enchufar una implementación personalizada (ver los javadocs de la clase
SQLExceptionConverterFactory para los detalles). Los subtipos estándar de
JDBCException son:
JDBCConnectionException - indica un error con la comunicación JDBC subyacente.
SQLGrammarException - indica un problema de gramática o sintáxis con el
SQL publicado.
ConstraintViolationException - indica alguna forma de violación de restricción
de integridad.
LockAcquisitionException - indica un error adquiriendo un nivel de bloqueo
necesario para realizar una operación solicitada.
GenericJDBCException - una excepción genérica que no cayó en ninguna de las
otras categorías.
Control optimista de concurrencia
El único enfoque que es consistente con alta concurrencia y alta escalabilidad es el control
optimista de concurrencia con versionamiento. El chuequeo de versión usa números de versión,
o timestamps, para detectar actualizaciones en conflicto (y para prevenir actualizaciones perdidas).
Hibernate provee para tres enfoques posibles de escribir código de aplicación que use concurrencia
optimista. Los casos de uso que hemos mostrado están en el contexto de transacciones de aplicación
largas pero el chequeo de versiones tiene además el beneficio de prevenir actualizaciones perdidas
en transacciones de base de datos solas.
Chequeo de versiones de aplicación
En una implementación sin mucha ayuda de Hibernate, cada interacción con la base de datos ocurre en una
nueva Session y el desarrollador es responsable de recargar todas las intancias
persistentes desde la base de datos antes de manipularlas. Este enfoque fuerza a la aplicación a
realizar su propio chequeo de versiones para asegurar el aislamiento de transacciones de base de datos.
Es el enfoque más similar a los EJBs de entidad.
La propiedad version se mapea usando <version>,
e Hibernate la incrementará automáticamente durante la limpieza si la entidad está sucia.
Por supuesto, si estás operando un entorno de baja-concurrencia-de-datos y no requieres
chequeo de versiones, puedes usar este enfoque y simplemente saltar el chequeo de versiones.
En ese caso, el último compromiso (commit) gana será la estrategia por
defecto para tus transacciones de aplicación largas. Ten en mente que esto podría confundir
a los usuarios de la aplicación, pues podrían experimentar actualizaciones perdidas sin
mensajes de error ni chance de fusionar los cambios conflictivos.
Claramente, el chequeo manual de versiones es factible solamente en circunstancias muy triviales,
y no es práctico para la mayoría de aplicaciones. Frecuentemente, no sólo intancias solas, sino grafos
completos de objetos modificados tienen que ser chequeados. Hibernate ofrece chequeo de versiones
automático con el paradigma de diseño de Session larga o de instancias separadas.
Sesión larga y versionado automático
Una sola instancia de Session y sus instancias persistentes
son usadas para toda la transacción de aplicación. Hibernate chequea las versiones
de instancia en el momento de limpieza (flush), lanzando una excepción si se detecta
una modificación concurrente. Concierne al desarrollador capturar y manejar esta excepción
(las opciones comunes son la oportunidad del usuario de fusionar los cambios, o recomenzar el
proceso de negocio sin datos añejos).
La Session se desconecta de cualquier conexión JDBC subyacente
al esperar por una interacción del usuario. Este enfoque es el más eficiente en términos
de acceso a base de datos. La aplicación no necesita tratar por sí misma con el chequeo de
versiones, ni re-uniendo instancias separadas, ni tiene que recargar instancias en cada
transacción de base de datos.
El objeto foo todavía conoce en qué Session fue cargado.
Session.reconnect() obtiene una nueva conexión (o puedes proveer una) y
reasume la sesión. El método Session.disconnect() desconectará la sesión
de la conexión JDBC y la devolverá la conexión al pool (a menos que hayas provisto la conexión).
Después de la reconexión, para forzar un chequeo de versión en datos que no estés actualizando,
puedes llamar a Session.lock() con LockMode.READ sobre
cualquier objeto que pudiese haber sido actualizado por otra transacción. No necesitas bloquear
ningún dato que sí estés actualizando.
Si las llamadas explícitas a disconnect() y reconnect()
son muy onerosas, puedes usar en cambio hibernate.connection.release_mode.
Este patrón es problemático si la Session es demasiado grande para ser almacenada
durante el tiempo de pensar del usuario, por ejemplo, una HttpSession debe
mantenerse tan pequeña como sea posible. Ya que la Session es también el caché
(obligatorio) de primer nivel y contiene todos los objetos cargados, podemos probablemente cargar
esta estrategia sólo para unos pocos ciclos petición/respuesta. Esto está de hecho recomendado, ya que
la Session tendrá pronto también datos añejos.
Nota también que debes mantener la Session desconectada próxima a la capa
de persistencia. En otras palabras, usa una sesión de EJB con estado para tener la
Session y no transferirla a la capa web para almacenarla en la
HttpSession (ni incluso serializarla a una capa separada).
Objetos separados y versionado automático
Cada interacción con el almacén persistente ocurre en una nueva Session.
Sin embargo, las mismas instancias persistentes son reusadas para cada interacción con la base de
datos. La aplicación manipula el estado de las instancias separadas originalmente cargadas en otra
Session y luego las readjunta usando Session.update(),
Session.saveOrUpdate(), o Session.merge().
De nuevo, Hibernate chequeará las versiones de instancia durante la limpieza (flush),
lanzando una excepción si ocurrieron actualizaciones en conflicto.
Puedes también llamar a lock() en vez de update()
y usar LockMode.READ (realizando un chequeo de versión, puenteando
todos los cachés) si estás seguro que el objeto no ha sido modificado.
Personalizando el versionado automático
Puedes deshabilitar el incremento de versión automático de Hibernate para propiedades en particular
y colecciones estableciendo el atributo de mapeo optimistic-lock a
false. Hibernate entonces no incrementará ya más las versiones si la propiedad está
sucia.
Los esquemas de base de datos heredados son frecuentemente estáticos y no pueden ser modificados.
U otras aplicaciones podrían también acceder la misma base de datos y no saber cómo manejar los números
de versión ni incluso timestamps. En ambos casos, el versionado no puede confiarse a una columna en
particular en una tabla. Para forzar un chequeo de versiones sin un mapeo de propiedad de versión o
timestamp, con una comparación del estado de todos los campos en una fila, activa
optimistic-lock="all" en el mapeo de <class>.
Nota que esto conceptualmente funciona solamente si Hibernate puede comparar el estado viejo y nuevo,
es decir, si usas una sola Session larga y no
sesión-por-petición-con-instancias-separadas.
A veces las modificaciones concurrentes pueden permitirse, en cuanto los cambios que hayan sido
hechos no se traslapen. Si estableces optimistic-lock="dirty" al mapear la
<class>, Hibernate sólo comparará los campos sucios durante la limpieza.
En ambos casos, con columnas de versión/timestamp dedicadas o con comparación de campos
completa/sucios, Hibernate usa una sola sentencia UPDATE
(con una cláusula WHERE apropiada) por entidad para ejecutar el chequeo
de versiones y actualizar la información. Si usas persistencia transitiva para la re-unión
en cascada de entidades asociadas, Hibernate podría ejecutar actualizaciones innecesarias.
Esto usualmente no es un problema, pero podrían ejecutarse disparadores (triggers)
on update en la base de datos incluso cuando no se haya hecho ningún cambio
a las instancias separadas. Puedes personalizar este comportamiento estableciendo
select-before-update="true" en el mapeo de <class>,
forzando a Hibernate a SELECT la instancia para asegurar que las actualizaciones
realmente ocurran, antes de actualizar la fila.
Bloqueo pesimista
No se pretende que los usuarios gasten mucho tiempo preocupándose de las estrategias de bloqueo.
Usualmente es suficiente con especificar un nivel de aislamiento para las conexiones JDBC y entonces
simplemente dejar que la base de datos haga todo el trabajo. Sin embargo, los usuarios avanzados pueden
a veces obtener bloqueos exclusivos pesimistas, o reobtener bloqueos al comienzo de una nueva
transacción.
¡Hibernate siempre usará el mecanismo de bloqueo de la base de datos, nunca bloqueo
de objetos en memoria!
La clase LockMode define los diferentes niveles de bloqueo que pueden ser adquiridos
por Hibernate. Un bloqueo se obtiene por los siguientes mecanismos:
LockMode.WRITE se adquiere automáticamente cuando Hibernate actualiza o
inserta una fila.
LockMode.UPGRADE puede ser adquirido bajo petición explícita del usuario
usando SELECT ... FOR UPDATE en base de datos que soporten esa sintáxis.
LockMode.UPGRADE_NOWAIT puede ser adquirido bajo petición explícita del usuario
usando un SELECT ... FOR UPDATE NOWAIT bajo Oracle.
LockMode.READ es adquirido automáticamente cuando Hibernate lee datos
bajo los niveles de aislamiento Repeatable Read o Serializable. Puede ser readquirido por
pedido explícito del usuario.
LockMode.NONE representa la ausencia de un bloqueo. Todos los objetos se pasan
a este modo de bloqueo al final de una Transaction. Los objetos asociados con una
sesión vía una llamada a update() o saveOrUpdate() también
comienzan en este modo de bloqueo.
La "petición explícita del usuario" se expresa en una de las siguientes formas:
Una llamada a Session.load(), especificando un LockMode.
Una llamada a Session.lock().
Una llamada a Query.setLockMode().
Si se llama a Session.load() con UPGRADE o
UPGRADE_NOWAIT, y el objeto pedido no ha sido aún cargado por la sesión, el objeto es
cargado usando SELECT ... FOR UPDATE. Si se llama a load() para
un objeto que ya esté cargado con un bloqueo menos restrictivo que el pedido, Hibernate llama a
lock() para ese objeto.
Session.lock() realiza un chequeo de número de versión si el modo de bloqueo especificado
es READ, UPGRADE o UPGRADE_NOWAIT. (En el caso de
UPGRADE o UPGRADE_NOWAIT, se usa
SELECT ... FOR UPDATE.)
Si la base de datos no soporta el modo de bloqueo solicitado, Hibernate usará un modo alternativo
apropiado (en vez de lanzar una excepción). Esto asegura que las aplicaciones serán portables.