Mejorando el rendimiento
Estrategias de recuperación
Una estrategia de recuperación es la estrategia que usará Hibernate para recuperar
los objetos asociados cuando la aplicación necesite navegar la asociación. Las estrategias de recuperación
pueden ser declaradas en los metadatos de mapeo O/R, o sobrescritas por una consulta HQL o
Criteria en particular.
Hibernate3 define las siguientes estrategias de recuperación:
Recuperación por unión (join fetching) - Hibernate recupera la
instancia asociada o colección en la misma SELECT, usando una
OUTER JOIN.
Recuperación por selección (select fetching) - se usa una segunda
SELECT para recuperar la entidad asociada o colección. A menos que
deshabilites explícitamente la recuperación perezosa especificando lazy="false",
la segunda selección sólo será ejecutada cuando realmente accedas a la asociación.
Recuperación por subselección (subselect fetching) - se usa una segunda
SELECT para recuperar las colecciones asociadas de todas las entidades
recuperadas en una consulta o recuperación previa. A menos que deshabilites explícitamente la
recuperación perezosa especificando lazy="false", esta segunda selección sólo
será ejecutada cuando realmente accedas a la asociación.
Recuperación en lote - una estrategia de optimización para la recuperación
por selección - Hibernate recupera un lote de instancias de entidad o colecciones en una sola
SELECT, especificando una lista de claves primarias o de claves foráneas.
Hibernate también distingue entre:
Recuperación inmediata - una asociación, colección o atributo es recuperado
inmediatamente, cuando el dueño es cargado.
Recuperación perezosa de colecciones - se recupera una colección cuando la
aplicación invoca una operación sobre la colección. (Esto es por defecto para las colecciones.)
Recuperación por proxy - se recupera una asociación monovaluada cuando se
invoca un método que no sea el getter del identificador sobre el objeto asociado.
Recuperación perezosa de atributos - se recupera un atributo o una asociación
monovaluada cuando se accede a la variable de instancia (requiere instrumentación del bytecode en
tiempo de ejecución). Este enfoque es raramente necesario.
Aquí tenemos dos nociones ortogonales: cuándo se recupera la aplicación,
y cómo es recuperada (qué SQL es usado). ¡No las confundas! Usamos
fetch para afinar el rendimiento. Podemos usar lazy para
definir un contrato sobre qué datos están siempre disponibles en cualquier instancia separada de
una clase en particular.
Trabajando con asociaciones perezosas
Por defecto, Hibernate3 usa una recuperación perezosa por selección para colecciones
y una recuperación por proxy perezosa para asociaciones monovaluadas. Estas políticas por
defecto tienen sentido para casi todas las asociaciones en casi todas las aplicaciones.
Nota: si estableces hibernate.default_batch_fetch_size, Hibernate
usará la optimización de recuperación en lotes para recuperación perezosa (esta optimización también puede
ser habilitada a un nivel más granularizado).
Sin embargo, la recuperación perezosa plantea un problema del que tienes que estar al tanto. Acceder
a una asociación perezosa fuera del contexto de una sesión de Hibernate abierta resultará en una
excepción. Por ejemplo:
Ya que la colección de permisos no fue inicializada cuando se cerró la Session,
la colección no será capaz de cargar su estado. Hibernate no soporta la inicialización
perezosa de objetos separados. La solución es mover el código que lee de la colección
a justo antes que la transacción sea comprometida.
Alternativamente, podríamos usar una colección no perezosa o asociación, especificando
lazy="false" para el mapeo de asociación. Sin embargo, está pensado
que la inicialización perezosa sea usada para casi todas las colecciones y asociaciones.
¡Si defines demasiadas asociaciones no perezosas en tu modelo de objetos, Hibernate terminará
necesitando recuperar la base de datos entera en cada transacción!
Por otro lado, frecuentemente necesitamos elegir la recuperación por unión (que es no perezosa
por naturaleza) en vez de la recuperación por selección en una transacción en particular. Veremos
ahora cómo personalizar la estrategia de recuperación. En Hibernate3, los mecanismos para elegir una
estrategia de recuperación son idénticas a las de las asociaciones monovaluadas y colecciones.
Afinando las estrategias de recuperación
La recuperación por selección (la preestablecida) es extremadamente vulnerable a problemas de
selección N+1, de modo querríamos habilitar la recuperación por unión (join fetching) en el
documento de mapeo:
]]>
La estrategia de recuperación definida en el documento de mapeo afecta a:
las recuperaciones vía get() o load()
las recuperaciones que ocurren implícitamente cuando se navega una asociación
(recuperación perezosa)
las consultas de Criteria
Usualmente, no usamos el documento de mapeo para personalizar la recuperación. En cambio, mantenemos el
comportamiento por defecto, y lo sobrescribimos para una transacción en particular, usando
left join fetch en HQL. Esto le dice a Hibernate que recupere la asociación
tempranamente en la primera selección, usando una unión externa. En la API de consulta de
Criteria, usarías setFetchMode(FetchMode.JOIN).
Si acaso lo deseases, podrías cambiar la estrategia de recuperación usada por
get() or load(); simplemente usa una consulta
Criteria, por ejemplo:
(Esto es el equivalente de Hibernate de lo que otras soluciones ORM llaman un "plan de recuperación".)
Una forma completamente diferente de evitar problemas con selecciones N+1 es usar el caché de
segundo nivel.
Proxies de asociaciones de un solo extremo
La recuperación perezosa de colecciones está implementada usando la implementación de colecciones
persistentes propia de Hibernate. Sin embargo, se necesita un mecanismo diferente para un comportamiento
perezoso en las asociaciones de un solo extremo. La entidad objetivo de la asociación debe ser tratada
con proxies. Hibernate implementa proxies de inicialización perezosa para objetos persistentes usando
mejora del bytecode en tiempo de ejecución (por medio de la excelente biblioteca CGLIB).
Por defecto, Hibernate3 genera proxies (en el arranque) para todas las clases persistentes y los usa
para habilitar la recuperación perezosa de asociaciones muchos-a-uno y
uno-a-uno.
El fichero de mapeo puede declarar una interface a usar como interface de proxy para esa clase,
con el atributo proxy. Por defecto, Hibernate usa una subclase de la clase.
Nota que la clase tratada con proxies debe implementar un constructor por defecto con al
menos visibilidad de paquete. ¡Recomendamos este constructor para todas las clases persistentes!
Hay algunos puntos a tener en cuenta al extender este enfoque a clases polimórficas, por ejemplo.
......
.....
]]>
Primero, las instancias de Cat nunca serán objeto de un cast a
DomesticCat, incluso aunque la instancia subyacente sea instancia de
DomesticCat:
Segundo, es posible romper con el operador == de un proxy.
Sin embargo, la situación no en absoluta tan mala como parece. Aunque tenemos ahora dos referencias
a objetos proxy diferentes, la instancia subyacente será aún el mismo objeto:
Tercero, no debes usar un proxy CGLIB para una clase final o una clase
con algún método final.
Finalmente, si tu objeto persistente adquiere cualquier recurso bajo instanciación
(por ejemplo, en inicializadores o constructores por defecto), entonces esos recursos
serán adquiridos también por el proxy. La clase del proxy es una subclase real de la clase
persistente.
Estos problemas se deben a limitaciones fundamentales en el modelo de herencia única de Java.
Si deseas evitar estos problemas cada una de tus clases persistentes deben implementar una
interface que declare sus métodos de negocio. Debes especificar estas interfaces en el fichero
de mapeo. Por ejemplo:
......
.....
]]>
donde CatImpl implementa la interface Cat y
DomesticCatImpl implementa la interface DomesticCat.
Entonces load() o iterate() pueden devolver instancias de
Cat y DomesticCat. (Nota que list()
usualmente no devuelve proxies.)
Las relaciones también son inicializadas perezosamente. Esto significa que debes declarar
cualquier propiedad como de tipo Cat, no CatImpl.
Ciertas operaciones no requieren inicialización de proxies.
equals(), si la clase persistente no sobrescribe equals()
hashCode(), si la clase persistente no sobrescribe
hashCode()
El método getter del identificador
Hibernate detectará las clase persistentes que sobrescriban equals() o
hashCode().
Inicializando colecciones y proxies
Una LazyInitializationException será lanzada por Hibernate si una colección
o proxy sin inicializar es accedido fuera del ámbito de la Session, es decir,
cuando la entidad que posee la colección o que tiene la referencia al proxy esté en el estado
separada.
A veces necesitamos asegurarnos que un proxy o colección esté inicializado antes de cerrar la
Session. Por supuesto, siempre podemos forzar la inicialización llamando a
cat.getSex() o cat.getKittens().size(), por ejemplo.
Pero esto es confuso a lectores del código y no es conveniente para código genérico.
Los métodos estáticos Hibernate.initialize() y
Hibernate.isInitialized() proveen a la aplicación de una forma conveniente de
trabajar con colecciones o proxies inicializados perezosamente.
Hibernate.initialize(cat) forzará la inicialización de un proxy,
cat, en tanto su Session esté todavía abierta.
Hibernate.initialize( cat.getKittens() ) tiene un efecto similar para la colección
de gatitos.
Otra opción es mantener la Session abierta hasta que todas las colecciones
y proxies necesarios hayan sido cargados. En algunas arquitecturas de aplicación, particularmente
en aquellas donde el código que accede a los datos usando Hibernate, y el código que los usa están
en capas de aplicación diferentes o procesos físicos diferentes, puede ser un problema asegurar que
la Session esté abierta cuando se inicializa una colección. Existen dos formas
básicas de tratar este tema:
En una aplicación basada web, puede usarse un filtro de servlets para cerrar la
Session sólo bien al final de una petición de usuario, una
vez que el rendering de la vista esté completa (el patrón Sesión Abierta en
Vista (Open Session in View)). Por supuesto, estos sitios requieren una
fuerte demanda de corrección del manejo de excepciones de tu infraestructura de
aplicación. Es de una importancia vital que la Session esté
cerrada y la transacción terminada antes de volver al usuario, incluso cuando ocurra
una excepción durante el rendering de la página. Para este enfoque, el filtro de servlet tiene
que ser capaz de accceder la Session. Recomendamos que se use una variable
ThreadLocal para tener la Session actual (ver
el capítulo 1, , para una implementación de ejemplo).
En una aplciación con una grada de negocios separada, la lógica de negocio debe "preparar"
todas las colecciones que se vayan a necesitar por la grada web antes de volver.
Esto significa que la grada de negocios debe cargar todos los datos y devolver a la grada de
presentación web todos los datos que se requieran para un caso de uso en particular
ya inicializados. Usualmente, la aplicación llama a Hibernate.initialize()
para cada colección que se necesitará en la grada web (esta llamada debe ocurrir antes que la
sesión sea cerrada) o recupera la colección tempranamente usando una consulta de Hibernate con una
cláusula FETCH o una FetchMode.JOIN en
Criteria. Esto es usualmente más fácil si adoptas el patrón
Comando en vez de un Fachada de Sesión.
Puedes también adjuntar un objeto cargado previamente a una nueva Session
con merge() o lock() antes de acceder a colecciones
no inicializadas (u otros proxies). ¡No, Hibernate no, y ciertamente no
debe hacer esto automáticamente, ya que introduciría semánticas de transacción ad hoc!
A veces no quieres inicializar una colección grande, pero necesitas aún alguna informacion sobre
ella (como su tamaño) o un subconjunto de los datos.
Puedes usar un filtro de colecciones para obtener el tamaño de una colección sin inicializarla:
El método createFilter() se usa también para recuperar eficientemente subconjuntos
de una colección sin necesidad de inicializar toda la colección:
Usando recuperación en lotes
Hibernate puede hacer un uso eficiente de la recuperación en lotes, esto es, Hibernate puede cargar
muchos proxies sin inicializar si se accede a un proxy (o colecciones). La recuperación en lotes es una
optimización de la estrategia de recuperación por selección perezosa. Hay dos formas en que puedes afinar
la recuperación en lotes: a nivel de la clase o de la colección.
La recuperación en lotes para clases/entidades es más fácil de entender. Imagina que tienes la siguiente
situación en tiempo de ejecución: Tienes 25 instancias de Cat cargadas en una
Session, cada Cat tiene una referencia a su owner,
una Person. La clase Person está mapeada con un proxy,
lazy="true". Si ahora iteras a través de todos los gatos y llamas a
getOwner() para cada uno, Hibernate por defecto ejecutará 25 sentencias
SELECT para traer los dueños tratados con proxies. Puedes afinar este comportamiento
especificando un batch-size en el mapeo de Person:
...]]>
Hibernate ahora ejecutará sólo tres consultas, el patrón es 10, 10, 5.
También puedes habilitar la recuperación en lotes para colecciones. Por ejemplo, si cada
Person tiene una colección perezosa de Cats, y hay 10
personas actualmente cargadas en la Session, iterar a través de las 10 personas
generará 10 SELECTs, una para cada llamada a getCats().
Si habilitas la recuperación en lotes para la colección de cats en el mapeo de
Person, Hibernate puede recuperar por adelantado las colecciones:
...
]]>
Con un batch-size de 3, Hibernate cargará 3, 3, 3, 1 colecciones en cuatro
SELECTs. Una vez más, el valor del atributo depende del número esperado de
colecciones sin inicializar en una Session en particular.
La recuperación de coleccione en lotes es particularmente útil si tienes un árbol anidado de
ítems, es decir, el típico patrón de cuenta de materiales. (Aunque un conjunto
anidado o una ruta materializada podría ser una mejor opción
para árboles que sean de lectura en la mayoría de los casos.)
Usando recuperación por subselección
Si una colección perezosa o proxy monovaluado tiene que ser recuperado, Hibernate los carga a todos,
volviendo a ejecutar la consulta original en una subselección. Esto funciona de la misma forma que
la recuperación en lotes, sin carga fragmentaria.
Usando recuperación perezosa de propiedades
Hibernate3 soporta la recuperación perezosa de propiedades individuales. Esta técnica de optimización
es también conocida como grupos de recuperación (fetch groups). Por favor, nota
que éste es mayormente un aspecto de marketing, ya que en la práctica, optimizar lecturas de filas es
mucho más importante que la optimización de lectura de columnas. Sin embargo, cargar sólo algunas
propiedades de una clase podría ser útil en casos extremos, cuando tablas heredadas tienen cientos de
columnas y el modelo de datos no puede ser mejorado.
Para habilitar la carga perezosa de propiedades, establece el atributo lazy en tus
mapeos de propiedades:
]]>
¡La carga perezosa de propiedades requiere la instrumentación del bytecode en tiempo
de construcción! Si tus clases persistentes no son mejoradas, Hibernate ignorará silenciosamente
la configuración perezosa de propiedades y caerá en recuperación inmediata.
Para la instrumentación del bytecode, usa la siguiente tarea Ant:
]]>
Una forma diferente (¿mejor?) de evitar lecturas innecesarias de columnas, al menos para
transacciones de sólo lectura es usar las funcionalidades de proyección de consultas HQL o Criteria.
Esto evita la necesidad de procesar el bytecode en tiempo de construcción y ciertamente es una solución
preferida.
Puedes forzar la usual recuperación temprana de propiedades usando fetch all properties
en HQL.
El Caché de Segundo Nivel
Una Session de Hibernate es una caché de datos persistentes a nivel de transacción.
Es posible configurar un cluster o caché a nivel de JVM (a nivel de SessionFactory)
sobre una base de clase-a-clase o colección-a-colección. Puedes incluso enchufar una caché en cluster.
Sé cuidadoso. Las cachés nunca están al tanto de los cambios hechos por otra aplicación al almacén persistente
(aunque pueden ser configurados para expirar regularmente los datos en caché).
Por defecto, Hibernate usa EHCache para caching a nivel de JVM. (El soporte a JCS ahora está despreciado
y será quitado en una futura versión de Hibernate.) Puedes elegir una implementación diferente estableciendo
el nombre de una clase que implemente org.hibernate.cache.CacheProvider usando la propiedad
hibernate.cache.provider_class.
Proveedores de Caché
Caché
clase del Provedor
Tipo
Cluster Seguro
Caché de Consultas Soportado
Hashtable (no pensado para uso en producción)
org.hibernate.cache.HashtableCacheProvider
memoria
sí
EHCache
org.hibernate.cache.EhCacheProvider
memoria, disco
sí
OSCache
org.hibernate.cache.OSCacheProvider
memoria, disco
sí
SwarmCache
org.hibernate.cache.SwarmCacheProvider
clusterizado (ip multicast)
sí (invalidación en cluster)
TreeCache de JBoss
org.hibernate.cache.TreeCacheProvider
clusterizado (ip multicast), transaccional
sí (replicación)
sí (requiere sincronización de reloj)
Mapeos de caché
El elemento <cache> de una mapeo de clase o colección tiene la siguiente
forma:
]]>
usage especifica la estrategia de caching:
transactional,
read-write,
nonstrict-read-write o
read-only
Alternativamente (¿preferiblemente?), puedes especificar los elementos
<class-cache> y <collection-cache> en
hibernate.cfg.xml.
El atributo usage especifica una estrategia de concurrencia al
caché.
Estrategia: sólo lectura (read only)
Si tu aplicación necesita leer pero nunca modificar las instancias de una clase persistente,
puede usarse un caché read-only. Esta es la mejor y más simple estrategia.
Es incluso perfectamente segura de usar en un cluster.
....
]]>
Estrategia: lectura/escritura (read/write)
Si la aplicación necesita actualizar datos, un caché read-write podría ser apropiado.
Esta estrategia de caché nunca debe ser usada si se requiere nivel de aislamiento serializable de
transacciones. Si el caché es usado en un entorno JTA, debes especificar la propiedad
hibernate.transaction.manager_lookup_class, mencionando una estrategia para obtener
el TransactionManager de JTA. En otros entornos, debes asegurarte que la transacción
esté completada cuando se llame a Session.close() o
Session.disconnect(). Si deseas usar esta estrategia en un cluster, debes asegurarte
que la implementación de caché subyacente soporta bloqueos. Los provedores de caché internos
predeterminados no no lo soportan.
....
....
]]>
Estrategia: lectura/escritura no estricta (nonstrict read/write)
Si la aplicación necesita sólo ocasionalmente actualizar datos (es decir, es extremadamente inprobable
que dos transacciones intenten actualizar el mismo ítem simultáneamente) y no se requiere de un
aislamiento de transacciones estricto, un caché nonstrict-read-write podría ser
apropiado. Si se usa el caché en un entorno JTA, debes especificar
hibernate.transaction.manager_lookup_class. En otros entornos, debes asegurarte que la
transacción se haya completado cuando se llame a Session.close() o
Session.disconnect().
Estrategia: transaccional
La estrategia de caché transactional brinda soporte a provedores de cachés
completamente transaccionales como TreeCache de JBoss. Un caché así, puede sólo ser usado en un
entorno JTA y debes especificar hibernate.transaction.manager_lookup_class.
Ninguno de los provedores de caché soporta todas las estrategias de concurrencia al caché. La siguiente
tabla muestra qué provedores son compatibles con qué estrategias de concurrencia.
Soporte a Estrategia de Concurrencia a Caché
Caché
read-only
nonstrict-read-write
read-write
transactional
Hashtable (no pensado para uso en producción)
sí
sí
sí
EHCache
sí
sí
sí
OSCache
sí
sí
sí
SwarmCache
sí
sí
JBoss TreeCache
sí
sí
Gestionando los cachés
Siempre que pases un objeto a save(), update()
o saveOrUpdate() y siempre que recuperes un objeto usando
load(), get(), list(),
iterate() o scroll(), ese objeto es agregado
al caché interno de la Session.
Cuando subsecuentemente se llame a flush(), el estado de ese objeto será
sincronizado con la base de datos. Si no quieres que ocurra esta sincronización o si estás
procesando un número enorme de objetos y necesitas gestionar la memoria eficientemente,
puede usarse el método evict() para quitar el objeto y sus colecciones
del caché de primer nivel.
La Session también provee un método contains() para determinar
si una instancia pertenece al caché de la sesión.
Para desahuciar (evict) todos los objetos del caché de sesión, llama a Session.clear().
Para el caché de segundo nivel, hay métodos definidos en SessionFactory para
desahuciar el estado en caché de una instancia, clase entera, instancia de colección o rol
enter de colección.
El CacheMode controla cómo una sesión en particular interactúa con el caché de segundo
nivel.
CacheMode.NORMAL - lee ítems desde y escribe ítems hacia el caché de segundo nivel
CacheMode.GET - lee ítems del caché de segundo nivel, pero no escribe al caché de
segundo nivel excepto al actualizar datos
CacheMode.PUT - escribe ítems al caché de segundo nivel, pero no lee del caché de segundo
nivel
CacheMode.REFRESH - escribe ítems al caché de segundo nivel, pero no lee del caché de
segundo nivel, saltándose el efecto de hibernate.cache.use_minimal_puts, forzando
un refresco del caché de segundo nivel para todos los ítems leídos de la base de datos
Para navegar por los contenidos de una región de caché de segundo nivel o de consultas, usa la API de
Statistics:
Necesitarás habilitar las estadísticas y, opcionalmente, forzar a Hibernate para que guarde las
entradas del caché en un formato más entendible por humanos:
El Caché de Consultas
Los conjuntos resultado de consultas también pueden tratarse en caché. Esto sólo es útil para
consultas que se ejecutan frecuentemente con los mismos parámetros. Para usar el caché de consultas
primero debes habilitarlo:
Esta configuración causa la creación de dos nuevas regiones de caché - una teniendo en caché
conjuntos resultado de consulta (org.hibernate.cache.StandardQueryCache),
el otro teniendo timestamps de las actualizaciones más recientes a tablas consultables
(org.hibernate.cache.UpdateTimestampsCache). Nota que el caché de consultas
no pone en caché el estado de las entidades reales en el conjunto resultado; sólo tiene en caché
valores indentificadores y resultados de tipo de valor. De modo que el caché de consultas siempre
debe ser usado en conjunción con el caché de segundo nivel.
La mayoría de consultas no se benefician del tratamiento en caché, de modo que por defecto las
consultas no son tratadas en caché. Para habilitar el tratamiento en caché, llama a
Query.setCacheable(true). Esta llamada permite a la consulta buscar
resultados existentes en caché o agregar sus resultados al caché cuando se ejecuta.
Si requieres un control finamente granularizado sobre las políticas de expiración del caché de
consultas, puedes especificar una región de caché con nombre para una consulta en particular
llamando a Query.setCacheRegion().
Si la consulta debe forzar un refresco de si región del caché de consultas, debes llamar a
Query.setCacheMode(CacheMode.REFRESH). Esto es particularmente útil en casos donde
los datos subyacentes pueden haber sido actualizados por medio de un proceso separado (es decir,
no modificados a través de Hibernate) y permite a la aplicación refrescar selectivamente conjuntos
resultado de consultas en particular. Esto es una alternativa más eficient al desahuciamiento de una
región del caché de consultas vía SessionFactory.evictQueries().
Entendiendo el rendimiento de Colecciones
Ya hemos llevado un buen tiempo hablando sobre colecciones.
En esta sección resaltaremos un par de temas más sobre cómo las colecciones
se comportan en tiempo de ejecución.
Taxonomia
Hibernate define tres tipos básicos de colecciones:
colecciones de valores
asociaciones uno a muchos
asociaciones muchos a muchos
Esta clasificación distingue las varias tablas y relaciones de clave foránea pero no nos
dice absolutamente todo lo que necesitamos saber sobre el modelo relacional. Para entender
completamente la estructura relacional y las características de rendimiento, debemos considerar
la estructura de la clave primaria que es usada por Hibernate para actualizar o borrar filas de
colección. Esto sugiere la siguiente clasificación:
colecciones indexadas
conjuntos (sets)
bolsas (bags)
Todas las colecciones indexadas (mapas, listas, arrays) tienen una clave primaria
consistente de las columnas <key> y <index>.
En este caso las actualizaciones de colecciones son usualmente extremadamente eficientes.
La clave primaria puede ser indexada fácilmente y una fila en particular puede ser localizada
cuando Hibernate intenta actualizarla o borrarla.
Los conjuntos (sets) tienen una clave primaria consistente en <key>
y columnas de elemento. Esto puede ser menos eficiente para algunos tipos de elemento de
colección, particularmente elementos compuestos o texto largo, o campos binarios. La base de datos
puede no ser capaz de indexar una clave primaria compleja eficientemente. Por otra parte,
para asociaciones uno a muchos o muchos a muchos, particularmente en el caso de identificadores
sintéticos, es probable que sólo sea tan eficiente. (Nota al márgen: si quieres que
SchemaExport realmente cree la clave primaria de un <set>
por ti, debes declarar todas las columnas como not-null="true".)
Los mapeos de <idbag> definen una clave delegada, de modo que siempre
resulten eficientes de actualizar. De hecho, son el mejor caso.
Los bags son el peor caso. Ya que un bag permite valores de elementos duplicados y no tiene
ninguna columna índice, no puede definirse ninguna clave primaria. Hibernate no tiene forma de
distinguir entre filas duplicadas. Hibernate resuelve este problema quitando completamente
(en un solo DELETE) y recreando la colección siempre que cambia.
Esto podría ser muy ineficiente.
Nota que para una asociación uno-a-muchos, la "clave primaria" puede no ser la clave
primaria física de la tabla de base de datos; pero incluso en este caso, la clasificación
anterior es útil todavía. (Aún refleja cómo Hibernate "localiza" filas individuales de la
colección.)
Las listas, mapas, idbags y conjuntos son las colecciones más eficientes de actualizar
Desde la discusión anterior, debe quedar claro que las colecciones indexadas y
(usualmente) los conjuntos permiten la operación más eficiente en términos de añadir,
quitar y actualizar elementos.
Hay, discutiblemente, una ventaja más que las colecciones indexadas tienen sobre otros
conjuntos para las asociaciones muchos a muchos o colecciones de valores. Debido a la
estructura de un Set, Hibernate ni siquiera actualiza una fila con
UPDATE cuando se "cambia" un elemento. Los cambios a un Set
siempre funcionan por medio de INSERT y DELETE
(de filas individuales). Una vez más, esta consideración no se aplica a las asociaciones
uno a muchos.
Después de observar que los arrays no pueden ser perezosos, podríamos concluir que las
listas, mapas e idbags son los tipos más eficientes de colecciones (no inversas), con los
conjuntos (sets) no muy por detrás. Se espera que los sets sean el tipo más común de colección
en las aplicaciones de Hibernate. Esto es debido a que la semántica de los sets es la más
natural en el modelo relacional.
Sin embargo, en modelos de dominio de Hibernate bien dieñados, usualmente vemos que la mayoría
de las colecciones son de hecho asociaciones uno-a-muchos con inverse="true".
Para estas asociaciones, la actualización es manejada por el extremo muchos-a-uno de la asociación,
y las consideraciones de este tipo sobre el rendimiento de actualización de colecciones simplemente
no se aplican.
Los Bags y las listas son las colecciones inversas más eficientes
Justo antes que tires a la zanja los bags para siempre, hay un caso en particular en el que
los bags son muchos más eficientes que los conjuntos. Para una colección con
inverse="true" (el idioma estándar de relaciones uno-a-muchos bidireccionales,
por ejemplo) ¡podemos añadir elementos a un bag o lista sin necesidad de inicializar (fetch)
los elementos del bag! Esto se debe a que Collection.add() o
Collection.addAll() siempre deben devolver true para un bag o List
(no como un Set). Esto puede hacer el siguiente código común mucho más rápido.
Borrado de un solo tiro
Ocasionalmente, borrar los elementos de una colección uno a uno puede ser extremadamente ineficiente.
Hibernate no es completamente estúpido, de modo que sabe no hacer eso, en el caso de una colección
nueva-vacía (si has llamado a list.clear(), por ejemplo). En este caso, Hibernate
publicará una sola DELETE, ¡y listo!
Supón que añadimos un solo elemento a una colección de tamaño veinte y luego quitamos dos elementos.
Hibernate publicará una sentencia INSERT y dos sentencias DELETE
(a menos que la colección sea un bag). Esto es ciertamente deseable.
Sin embargo, supón que quitamos dieciocho elementos, dejando dos y luego añadimos tres nuevos elementos.
Hay dos formas posibles de proceder
borrar dieciocho filas una a una y luego insertar tres filas
quitar toda la colección (en un solo DELETE de SQL) e insertar todos los
cinco elementos actuales (uno a uno)
Hibernate no es lo suficientemente inteligente para saber que la segunda opción es probablemente más
rápida en este caso. (Y que sería probablemente indeseable para Hibernate ser tan inteligente;
este comportamiento podría confundir a disparadores de base de datos, etc.)
Afortunadamente, puedes forzar este comportamiento (es decir, la segunda estrategia) en cualquier
momento descartando (es decir, desreferenciando) la colección original y devolviendo una colección
nuevamente instanciada con todos los elementos actuales. Esto puede ser muy útil y potente de vez en
cuando.
Por supuesto, el borrado-de-un-solo-tiro no se aplica a colecciones mapeadas
inverse="true".
Monitoreando el rendimiento
La optimización no es de mucho uso sin el monitoreo y el acceso a números de rendimiento. Hibernate provee
un rango completo de figuras sobre sus operaciones internas. Las estadísticas en Hibernate están disponibles
por SessionFactory.
Monitoreando una SessionFactory
Puedes acceder a las métricas de SessionFactory de dos formas.
Tu primera opción es llamar a sessionFactory.getStatistics() y
leer o mostrar por pantalla la Statistics por ti mismo.
Hibernate puede también usar JMX para publicar las métricas si habilitas el MBean
StatisticsService. Puede habilitar un solo MBean para todas tus
SessionFactory o una por fábrica. Mira el siguiente código para
ejemplos de configuración minimalistas:
POR HACER: Esto no tiene sentido: En el primer caso, recuperamos y usamos el MBean directamente.
En el segundo, debemos proporcionar el nombre JNDI en el que se guarda la fábrica de sesiones antes
de usarlo. Usa hibernateStatsBean.setSessionFactoryJNDIName("my/JNDI/Name")
Puedes (des)activar el monitoreo de una SessionFactory
en tiempo de configuración, establece hibernate.generate_statistics a
false
en tiempo de ejecución: sf.getStatistics().setStatisticsEnabled(true)
o hibernateStatsBean.setStatisticsEnabled(true)
Las estadísticas pueden ser reajustadas programáticamente usando el método clear().
Puede enviarse un resumen a un logger (nivel info) usando el método logSummary().
Métricas
Hibernate provee un número de métricas, desde información muy básica a la especializada
sólo relevante en ciertos escenarios. Todos los contadores disponibles se describen en la
API de la interface Statistics, en tres categorías:
Métricas relacionadas al uso general de Session usage, tales como
número de sesiones abiertas, conexiones JDBC recuperadas, etc,
Métricas relacionadas a las entidades, colecciones, consultas, y cachés como un todo.
(también conocidas como métricas globales).
Métricas detalladas relacionadas a una entidad, colección, consulta o región de caché
en particular.
Por ejemplo, puedes comprobar el acceso, pérdida, y radio de colocación de entidades, colecciones
y consultas en el caché, y el tiempo promedio que necesita una consulta. Ten en cuenta que el número
de milisegundos está sujeto a aproximación en Java. Hibernate está pegado a la precisión de la JVM,
en algunas plataformas esto podría incuso ser tener sólo una exactitud de 10 segundos.
Se usan getters simples para acceder a las métricas globales (es decir, no pegadas a una entidad,
colección, región de caché, etc, en particular). Puedes acceder a las métricas de una entidad,
colección, región de caché en particular a través de su nombre, y a través de su representación HQL
o SQL para las consultas. Por favor refiérete al Javadoc de la API de Statistics,
EntityStatistics, CollectionStatistics,
SecondLevelCacheStatistics, y QueryStatistics para más información.
El siguiente código muestra un ejemplo sencillo:
Para trabajar sobre todas las entidades, colecciones, consultas y regiones de cachés, puedes recuperar la
lista de nombres de entidades, colecciones, consultas y regiones de cachés con los siguientes métodos:
getQueries(), getEntityNames(),
getCollectionRoleNames(), y getSecondLevelCacheRegionNames().