implementaciones hibernate jpa

hibernate - implementaciones - jpa java



Hibernate consulta mucho más lento con flushMode=AUTO hasta que se llame a clear() (2)

Tengo una aplicación de larga ejecución (pero bastante simple) que utiliza Hibernate (a través de JPA). Estaba experimentando una desaceleración bastante dramática mientras corría. He sido capaz de entityManager.clear() a requerir una entityManager.clear() ocasional de entityManager.clear() . Cuando el administrador de entidades de Hibernate rastrea 100.000 entidades, es aproximadamente 100 veces más lento que cuando rastrea solo unas pocas (vea los resultados a continuación). Mi pregunta es: ¿por qué Hiberate se ralentiza tanto cuando rastrea muchas entidades? ¿Y hay otras formas de evitarlo?

!!! Actualización: he podido reducir esto al código de lavado automático de Hibernate. !!!

Específicamente para el org.hibernate.event.internal.AbstractFlushingEventListener flushEntities() (al menos en Hibernate 4.1.1.Final). En él hay un bucle que itera sobre TODAS las entidades en el contexto de persistencia, realizando algunas comprobaciones exhaustivas para vaciar cada una de ellas (¡aunque todas las entidades ya están vacías en mi ejemplo!).

Entonces, respondiendo parcialmente a la segunda parte de mi pregunta, el problema de rendimiento se puede solucionar configurando el modo de descarga a FlushModeType.COMMIT en la consulta (consulte los resultados actualizados a continuación). p.ej

Place place = em.createQuery("from Place where name = :name", Place.class) .setParameter("name", name) .setFlushMode(FlushModeType.COMMIT) // <-- yay! .getSingleResult();

... pero esto parece una solución bastante desagradable: pasar la responsabilidad de saber si las cosas se descargan a los métodos de consulta en lugar de mantenerlos en los métodos de actualización. También significa que tengo que configurar el modo de descarga en COMPRAR en todos los métodos de consulta o, más probablemente, configurarlo en el EntityManager.

Esto me hace preguntarme: ¿es este comportamiento esperado? ¿Estoy haciendo algo mal con el rubor o cómo defino entidades? ¿O es esto una limitación de (o posiblemente un error en) Hibernación?

El código de ejemplo que utilicé para aislar el problema es el siguiente:

La entidad de prueba

@Entity @Table(name="place") @Immutable public class Place { private Long _id; private String _name; @Id @GeneratedValue public Long getId() { return _id; } public void setId(Long id) { _id = id; } @Basic(optional=false) @Column(name="name", length=700, updatable=false, nullable=false, unique=true, columnDefinition="varchar(700) character set ''ascii'' not null") public String getName() { return _name; } public void setName(String name) { _name = name; } @Override public boolean equals(Object o) { /* ... */ } @Override public int hashCode() { return getName().hashCode(); } }

El código de referencia

El código de prueba que tengo genera 100000 nombres de lugares aleatorios y los inserta. Luego consulta a 5000 de esos al azar por nombre. Hay un índice en la columna de nombre.

Place place = em.createQuery( "select p from Place p where p.name = :name", Place.class) .setParameter("name", name) .getSingleResult();

A modo de comparación, y para asegurarme de que no estuviera en la base de datos, ejecuté la siguiente consulta basada en JDBC (bajo em.unwrap(Session.class).doWork(...) ) sobre un nombre de 5000 lugares seleccionados al azar. :

PreparedStatement ps = c.prepareStatement( "select id, name from place where name = ?"); ps.setString(1, name); ResultSet rs = ps.executeQuery(); while (rs.next()) { Place place = new Place(); place.setId(rs.getLong(1)); place.setName(rs.getString(2)); } rs.close(); ps.close();

(Tenga en cuenta que creo y cierro una declaración preparada para cada una de las 5000 consultas para el punto de referencia).

Los resultados

Todos los resultados a continuación tienen un promedio de más de 5000 consultas. La JVM fue dada -Xmx1G

Seconds/Query Approach 0.000160s JDBC 0.000286s Hibernate calling clear() after import and every 100 queries 0.000653s Hibernate calling clear() once after the import 0.012533s Hibernate w/o calling clear() at all 0.000292s Hibernate w/o calling clear(), and with flush-mode COMMIT

Otras observaciones: durante las consultas de Hibernate (sin ninguna llamada clara), el proceso Java conectó un núcleo a una utilización cercana al 100%. La JVM nunca superó los 500 MB de pila. También hubo mucha actividad de GC durante las consultas, pero la utilización de la CPU estaba claramente dominada por el código de Hibernate.


Pero, principalmente, siento curiosidad por el hecho de que Hibernate parece exhibir O (n) o incluso O (n ^ 2) búsquedas para las consultas; parece que debería poder usar una tabla hash o un árbol binario debajo del capó para mantener las consultas. rápido. Observe la diferencia de 2 órdenes de magnitud cuando su seguimiento de 100000 entidades frente a 100 entidades.

La complejidad de O (n²) se debe a la forma en que se debe manejar una consulta. Dado que Hibernate aplazó internamente las actualizaciones y las inserciones, siempre que sea posible (para aprovechar la posibilidad de agrupar actualizaciones / inserciones similares, especialmente si establece varias propiedades de un objeto).

Por lo tanto, antes de poder guardar de forma segura los objetos en la base de datos, Hibernate tiene que detectar todos los cambios en los objetos y eliminar todos los cambios. El problema aquí es que la hibernación también tiene alguna notificación e intercepción en curso. Así que itera sobre cada objeto de entidad gestionado por el contexto de persistencia. Incluso si el objeto en sí mismo no es mutable, podría contener objetos mutables o incluso colecciones de referencia.

Además, el mecanismo de intercepción incluye a cualquier objeto que se considera sucio para permitir que su propio código implemente verificaciones de suciedad adicionales o realice cálculos adicionales como calcular sumas, valores promedio, registrar información adicional, etc.

Pero veamos el código por un minuto:

La llamada al ras para preparar los resultados de la consulta en:

DefaultFlushEventListener.onFlush(..)

-> AbstractFlushingEventListener.flushEverythingToExecution (evento) -> AbstractFlushingEventListener.prepareEntityFlushes (..)

La implementación utiliza:

for ( Map.Entry me : IdentityMap.concurrentEntries( persistenceContext.getEntityEntries() ) ) { EntityEntry entry = (EntityEntry) me.getValue(); Status status = entry.getStatus(); if ( status == Status.MANAGED || status == Status.SAVING || status == Status.READ_ONLY ) { cascadeOnFlush( session, entry.getPersister(), me.getKey(), anything ); } }

Como puede ver, se recupera e itera un mapa de todas las entidades en el contexto de persistencia.

Eso significa que por cada invocación de una consulta, se itera sobre todos los resultados anteriores para verificar si hay objetos sucios. Y aún más cascadeOnFlush crea un nuevo Objeto y hace aún más cosas. Aquí está el código de cascadeOnFlush:

private void cascadeOnFlush(EventSource session, EntityPersister persister, Object object, Object anything) throws HibernateException { session.getPersistenceContext().incrementCascadeLevel(); try { new Cascade( getCascadingAction(), Cascade.BEFORE_FLUSH, session ) .cascade( persister, object, anything ); } finally { session.getPersistenceContext().decrementCascadeLevel(); } }

Así que esta es la explicación. Hibernate solo verifica cada objeto administrado por el contexto de persistencia cada vez que emite una consulta.

Entonces, para todos los que lean esto aquí es el cálculo de complejidad: 1. Consulta: 0 entidades 2. Consulta: 1 entidad 3. Consulta: 2 entidades .. 100. Consulta: 100 entidades. .. 100k + 1 consulta: 100k entradas

Entonces tenemos O (0 + 1 + 2 ... + n) = O (n (n + 1) / 2) = O (n²).

Esto explica tu observación. Con el fin de mantener un tamaño reducido de CPU y memoria, el contexto de persistencia administrada debe mantenerse lo más pequeño posible. Dejar que Hibernate administre más de lo que digamos 100 o 1000 entidades ralentiza considerablemente Hibernate. Aquí se debe considerar cambiar el modo de descarga, usar una segunda sesión para la consulta y otra para el cambio (si es posible) o usar StatelessSession.

Así que tu observación es correcta, es O (n²) en curso.


Quizás esté familiarizado con que EntityManager realiza un seguimiento de los objetos persistentes (es decir, los creados al llamar a em.createQuery(...).getSingleResult() ). Se acumulan en el llamado contexto o sesión persistente (el término de Hibernate) y permite características muy claras . Por ejemplo, puede modificar el objeto llamando al método mutator setName(...) y el EntityManager sincronizará este cambio de estado en la memoria con la base de datos (emitirá una instrucción UPDATE) siempre que sea apropiado. Esto sucede sin necesidad de llamar a métodos explícitos de save() o update() . Todo lo que necesita es trabajar con el objeto como si fuera un objeto Java normal y EntityManager se encargará de la persistencia.

¿Por qué esto es lento (er)?

Por un lado, garantiza que solo hay una instancia única por clave principal en la memoria. Esto significa que si carga una y la misma fila dos veces, solo habrá un objeto creado en el montón (ambos resultados serán == ). Esto tiene mucho sentido: imagínese si tiene 2 copias de la misma fila, EntityManager no puede garantizar que sincronice de manera confiable el objeto Java, ya que puede hacer cambios en ambos objetos de manera independiente. Quizás hay muchas otras operaciones de bajo nivel que eventualmente ralentizan el Entitymanager si hay muchos objetos que deben ser rastreados. Los métodos clear() realidad eliminan el contexto persistente de los objetos y facilitan la tarea (menos objetos para rastrear = operación más rápida).

¿Cómo puedes evitarlo?

Si su implementación de EntityManager es Hibernate, puede usar StatelessSession que está diseñado para solucionar estas penalizaciones de rendimiento. Creo que puedes conseguirlo a través de

StatelessSession session = ((Session) entityManager.getDelegate()).getSessionFactory().openStatelessSession();

(NB! Código no probado, tomado de otra question )