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



Answers

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.

Question

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).




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.




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.




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 :)






Related