java hibernate - JPA: ¿cuál es el patrón adecuado para iterar sobre grandes conjuntos de resultados?





example maven (12)


No se puede hacer esto en JPA directo, sin embargo Hibernate tiene soporte para sesiones sin estado y conjuntos de resultados desplazables.

Rutinariamente procesamos miles de millones de filas con su ayuda.

Aquí hay un enlace a la documentación: http://docs.jboss.org/hibernate/core/3.3/reference/en/html/batch.html#batch-statelesssession

Digamos que tengo una tabla con millones de filas. Usando JPA, ¿cuál es la forma correcta de iterar sobre una consulta en esa tabla, de modo que no tengo toda una lista en memoria con millones de objetos?

Por ejemplo, sospecho que lo siguiente explotará si la tabla es grande:

List<Model> models = entityManager().createQuery("from Model m", Model.class).getResultList();

for (Model model : models)
{
     System.out.println(model.getId());
}

¿La paginación ( setFirstResult() y actualizar manualmente setFirstResult() / setMaxResult() ) es realmente la mejor solución?

Editar : el principal caso de uso al que me estoy dirigiendo es una especie de trabajo por lotes. Está bien si lleva mucho tiempo correr. No hay un cliente web involucrado; Solo necesito "hacer algo" por cada fila, una (o alguna N pequeña) a la vez. Solo trato de evitar tenerlos todos en la memoria al mismo tiempo.




Puedes usar otro "truco". Cargue solo una colección de identificadores de las entidades que le interesan. Say identifier es de tipo long = 8bytes, luego 10 ^ 6 una lista de dichos identificadores hace alrededor de 8Mb. Si se trata de un proceso por lotes (una instancia a la vez), entonces es soportable. Luego solo itera y haz el trabajo.

Una observación más: de todos modos debes hacer esto en porciones, especialmente si modificas los registros; de lo contrario, el segmento de reversión en la base de datos crecerá.

Cuando se trata de establecer la estrategia firstResult / maxRows, será MUY MUY lenta para los resultados lejos de la parte superior.

También tenga en cuenta que la base de datos probablemente está funcionando en aislamiento de lectura confirmada , por lo que para evitar fantasmas lee los identificadores de carga y luego carga las entidades una por una (o 10 por 10 o lo que sea).




Depende del tipo de operación que tengas que hacer. ¿Por qué estás recorriendo más de un millón de filas? ¿Estás actualizando algo en modo batch? ¿Vas a mostrar todos los registros a un cliente? ¿Estás calculando algunas estadísticas sobre las entidades recuperadas?

Si va a mostrar un millón de registros al cliente, reconsidere su interfaz de usuario. En este caso, la solución adecuada es paginar los resultados y usar setFirstResult() y setMaxResult() .

Si ha lanzado una actualización de una gran cantidad de registros, será mejor que mantenga la actualización simple y utilice Query.executeUpdate() . Opcionalmente, puede ejecutar la actualización en modo asíncrono utilizando un Bean oa Work Manager controlado por mensajes.

Si está calculando algunas estadísticas sobre las entidades recuperadas, puede aprovechar las funciones de agrupamiento definidas por la especificación JPA.

Para cualquier otro caso, sea más específico :)




No hay una solución "adecuada" para hacer esto, esto no es lo que JPA o JDO o cualquier otro ORM está destinado a hacer, directamente JDBC será su mejor alternativa, ya que puede configurarlo para traer de vuelta un pequeño número de filas en un tiempo y eliminarlos a medida que se utilizan, es por eso que existen cursores del lado del servidor.

Las herramientas ORM no están diseñadas para procesamiento masivo, están diseñadas para permitirle manipular objetos e intentar que el RDBMS en el que se almacenan los datos sea lo más transparente posible, la mayoría falla en la parte transparente al menos hasta cierto punto. En esta escala, no hay forma de procesar cientos de miles de filas (Objetos), mucho menos millones con cualquier ORM y hacer que se ejecute en un período de tiempo razonable debido a la sobrecarga de la instanciación del objeto, simple y llanamente.

Usa la herramienta apropiada. Los procedimientos correctos JDBC y almacenados definitivamente tienen un lugar en 2011, especialmente en lo que son mejores en comparación con estos marcos ORM.

Sacar un millón de cualquier cosa, incluso en un simple List<Integer> no va a ser muy eficiente, independientemente de cómo lo haga. La forma correcta de hacer lo que está pidiendo es una simple SELECT id FROM table , establecer en SERVER SIDE (dependiente del vendedor) y el cursor en FORWARD_ONLY READ-ONLY e iterar sobre eso.

Si realmente está procesando millones de id para procesar llamando a cada servidor web con un servidor web, también tendrá que hacer un procesamiento concurrente para que se ejecute en un período de tiempo razonable. Tirando con un cursor JDBC y colocando algunos de ellos a la vez en ConcurrentLinkedQueue y teniendo un pequeño grupo de hilos (# CPU / Cores + 1) extrayéndolos y procesándolos es la única manera de completar su tarea en una máquina con cualquier " normal "cantidad de RAM, dado que ya se está quedando sin memoria.

Vea esta answer también.




Para ser honesto, sugiero dejar JPA y seguir con JDBC (pero ciertamente usando la clase de soporte JdbcTemplate o algo así). JPA (y otros proveedores / especificaciones ORM) no está diseñado para operar en muchos objetos dentro de una transacción, ya que suponen que todo lo cargado debe permanecer en la memoria caché de primer nivel (de ahí la necesidad de clear() en JPA).

También recomiendo más solución de bajo nivel porque la sobrecarga de ORM (la reflexión es solo la punta de un iceberg) puede ser tan importante, que iterar sobre ResultSet formato, incluso utilizando algún soporte liviano como el mencionado JdbcTemplate será mucho más rápido.

JPA simplemente no está diseñado para realizar operaciones en una gran cantidad de entidades. Puede jugar con flush() / clear() para evitar OutOfMemoryError , pero considere esto una vez más. Ganas muy poco pagando el precio del gran consumo de recursos.




Para ampliar la respuesta de @Tomasz Nurkiewicz. Usted tiene acceso a DataSource que a su vez puede proporcionarle una conexión

@Resource(name = "myDataSource",
    lookup = "java:comp/DefaultDataSource")
private DataSource myDataSource;

En tu código tienes

try (Connection connection = myDataSource.getConnection()) {
    // raw jdbc operations
}

Esto le permitirá omitir el JPA para algunas operaciones específicas de lotes grandes como importación / exportación, sin embargo, usted todavía tiene acceso al administrador de entidades para otras operaciones de JPA si lo necesita.




Utilice el concepto de paginación para recuperar el resultado




Con Hibernate hay 4 formas diferentes de lograr lo que desea. Cada uno tiene concesiones de diseño, limitaciones y consecuencias. Sugiero explorar cada uno y decidir cuál es el adecuado para su situación.

  1. Usar sesión sin estado con desplazamiento ()
  2. Use session.clear () después de cada iteración. Cuando se deben adjuntar otras entidades, cárguelas en una sesión separada. efectivamente, la primera sesión emula la sesión sin estado, pero conserva todas las características de una sesión con estado, hasta que los objetos se separan.
  3. Use iterate () o list () pero obtenga solo ids en la primera consulta, luego en una sesión separada en cada iteración, haga session.load y cierre la sesión al final de la iteración.
  4. Utilice Query.iterate () con EntityManager.detach () aka Session.evict ();



La página 537 de Java Persistence with Hibernate ofrece una solución con ScrollableResults , pero, por desgracia, es solo para Hibernate.

Por lo tanto, parece que el uso de setFirstResult / setMaxResults y la iteración manual realmente es necesario. Aquí está mi solución usando JPA:

private List<Model> getAllModelsIterable(int offset, int max)
{
    return entityManager.createQuery("from Model m", Model.class).setFirstResult(offset).setMaxResults(max).getResultList();
}

entonces, úsalo así:

private void iterateAll()
{
    int offset = 0;

    List<Model> models;
    while ((models = Model.getAllModelsIterable(offset, 100)).size() > 0)
    {
        entityManager.getTransaction().begin();
        for (Model model : models)
        {
            log.info("do something with model: " + model.getId());
        }

        entityManager.flush();
        entityManager.clear();
        em.getTransaction().commit();
        offset += models.size();
    }
}



Me he preguntado esto yo mismo. Parece importar:

  • qué tan grande es su conjunto de datos (filas)
  • qué implementación de JPA estás usando
  • qué tipo de procesamiento está haciendo para cada fila.

He escrito un iterador para facilitar el intercambio de ambos enfoques (findAll vs findEntries).

Te recomiendo que pruebes ambos.

Long count = entityManager().createQuery("select count(o) from Model o", Long.class).getSingleResult();
ChunkIterator<Model> it1 = new ChunkIterator<Model>(count, 2) {

    @Override
    public Iterator<Model> getChunk(long index, long chunkSize) {
        //Do your setFirst and setMax here and return an iterator.
    }

};

Iterator<Model> it2 = List<Model> models = entityManager().createQuery("from Model m", Model.class).getResultList().iterator();


public static abstract class ChunkIterator<T> 
    extends AbstractIterator<T> implements Iterable<T>{
    private Iterator<T> chunk;
    private Long count;
    private long index = 0;
    private long chunkSize = 100;

    public ChunkIterator(Long count, long chunkSize) {
        super();
        this.count = count;
        this.chunkSize = chunkSize;
    }

    public abstract Iterator<T> getChunk(long index, long chunkSize);

    @Override
    public Iterator<T> iterator() {
        return this;
    }

    @Override
    protected T computeNext() {
        if (count == 0) return endOfData();
        if (chunk != null && chunk.hasNext() == false && index >= count) 
            return endOfData();
        if (chunk == null || chunk.hasNext() == false) {
            chunk = getChunk(index, chunkSize);
            index += chunkSize;
        }
        if (chunk == null || chunk.hasNext() == false) 
            return endOfData();
        return chunk.next();
    }

}

Terminé no usando mi iterador de fragmento (por lo que podría no ser tan probado). Por cierto, necesitarás colecciones de google si quieres usarlo.




Probé las respuestas que aquí se presentan, pero JBoss 5.1 + MySQL Connector / J 5.1.15 + Hibernate 3.3.2 no funcionó con esas. Acabamos de migrar de JBoss 4.x a JBoss 5.1, así que lo hemos mantenido por ahora, y por lo tanto, el último Hibernate que podemos usar es 3.3.2.

Agregar dos parámetros adicionales hizo el trabajo, y un código como este se ejecuta sin OOMEs:

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

        Query query = session
                .createQuery("SELECT a FROM Address a WHERE .... ORDER BY a.id");
        query.setFetchSize(Integer.valueOf(1000));
        query.setReadOnly(true);
        query.setLockMode("a", LockMode.NONE);
        ScrollableResults results = query.scroll(ScrollMode.FORWARD_ONLY);
        while (results.next()) {
            Address addr = (Address) results.get(0);
            // Do stuff
        }
        results.close();
        session.close();

Las líneas cruciales son los parámetros de consulta entre createQuery y scroll. Sin ellos, la llamada de "desplazamiento" intenta cargar todo en la memoria y nunca termina o se ejecuta en OutOfMemoryError.




La especificación JPA dice lo siguiente sobre persist() .

Si X es un objeto separado, se puede EntityExistsException la EntityExistsException cuando se invoca la operación de persistencia, o la EntityExistsException u otra PersistenceException se pueden lanzar al momento de la descarga o de confirmación.

Por lo tanto, usar persist() sería adecuado cuando el objeto no debería ser un objeto separado. Es posible que prefiera que el código lance la PersistenceException para que falle rápidamente.

Aunque la especificación no es clara , persist() podría establecer el @GeneratedValue @Id para un objeto. Sin embargo, merge() debe tener un objeto con el @Id ya generado.





java hibernate jpa