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


114

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 tenga toda una lista en memoria con millones de objetos?

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

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

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

¿Es la paginación (bucle y actualización manual setFirstResult()/setMaxResult() ) realmente la mejor solución?

Editar : el caso de uso principal al que me dirijo es una especie de trabajo por lotes. Está bien si tarda mucho en ejecutarse. No hay ningún cliente web involucrado; Solo necesito "hacer algo" para cada fila, una (o una pequeña N) a la vez. Solo intento evitar tenerlos todos en la memoria al mismo tiempo.


¿Qué base de datos y controlador JDBC está utilizando?

Respuestas:


55

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

Entonces parece que usar setFirstResult/ setMaxResultsy 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();
}

luego, ú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();
    }
}

33
Creo que el ejemplo no es seguro si hay nuevos insertos durante el proceso por lotes. El usuario debe ordenar en base a una columna en la que esté seguro de que los datos recién insertados estarán al final de la lista de resultados.
Balazs Zsoldos

cuando la página actual es la última página y tiene menos de 100 elementos, la verificación size() == 100se saltará una consulta adicional que devuelve una lista vacía
cdalxndr

38

Probé las respuestas presentadas aquí, pero JBoss 5.1 + MySQL Connector / J 5.1.15 + Hibernate 3.3.2 no funcionó con ellas. Acabamos de migrar de JBoss 4.xa JBoss 5.1, así que nos hemos quedado con él por ahora y, por lo tanto, la última versión de Hibernate que podemos usar es la 3.3.2.

Agregar un par de 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 "scroll" intenta cargar todo en la memoria y nunca termina o se ejecuta en OutOfMemoryError.


2
Hola Zds, tu caso de uso de escanear millones de filas es ciertamente común para mí, y GRACIAS por publicar el código final. En mi caso, estoy introduciendo registros en Solr, para indexarlos para la búsqueda de texto completo. Y, debido a las reglas comerciales en las que no entraré, necesito pasar por Hibernate, en lugar de simplemente usar JDBC o los módulos integrados de Solr.
Mark Bennett

Feliz de ayudar :-). También estamos tratando con grandes conjuntos de datos, en este caso, lo que permite al usuario consultar todos los nombres de las calles dentro de la misma ciudad / condado o, a veces, incluso en el estado, por lo que la creación de índices requiere leer muchos datos.
Zds

Aparece con MySQL, realmente tienes que pasar por todos esos aros: stackoverflow.com/a/20900045/32453 (otras bases de datos podrían ser menos estrictas, imagino ...)
rogerdpack

32

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

Procesamos rutinariamente 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


17
Gracias. Es bueno saber que alguien está haciendo miles de millones de filas a través de Hibernate. Algunas personas aquí afirman que es imposible. :-)
George Armhold

2
¿Es posible agregar un ejemplo aquí también? ¿Supongo que es similar al ejemplo de Zds?
rogerdpack

19

Para ser honesto, sugeriría dejar JPA y seguir con JDBC (pero ciertamente usando JdbcTemplateclases de soporte o similares). JPA (y otros proveedores / especificaciones de ORM) no está diseñado para operar en muchos objetos dentro de una transacción, ya que asumen que todo lo cargado debe permanecer en la caché de primer nivel (de ahí la necesidad declear() JPA).

También recomiendo una solución de nivel más bajo porque la sobrecarga de ORM (la reflexión es solo la punta de un iceberg) podría ser tan significativa, que iterar sobre el plano ResultSet, incluso usar algún soporte liviano como el mencionadoJdbcTemplate , 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 de un enorme consumo de recursos.


La ventaja de JPA es no solo ser independiente de la base de datos, sino la posibilidad de ni siquiera usar una base de datos tradicional (NoSQL). No es difícil hacer el vaciado / limpieza de vez en cuando y, por lo general, las operaciones por lotes se realizan con poca frecuencia.
Adam Gent

1
Hola Thomasz. Tengo muchas razones para quejarme de JPA / Hibernate, pero respetuosamente, realmente dudo que "no estén diseñadas para operar en muchos objetos". Sospecho que solo necesito aprender el patrón adecuado para este caso de uso.
George Armhold

4
Bueno, solo puedo pensar en dos patrones: paginaciones (mencionadas varias veces) y flush()/ clear(). En mi humilde opinión, el primero no está diseñado para fines de procesamiento por lotes, mientras que el uso de la secuencia de flush () / clear () huele a abstracción con fugas .
Tomasz Nurkiewicz

Sí, fue una combinación de paginación y flush / clear como mencionaste. ¡Gracias!
George Armhold

7

Si usa EclipseLink I 'usando este método para obtener el resultado como Iterable

private static <T> Iterable<T> getResult(TypedQuery<T> query)
{
  //eclipseLink
  if(query instanceof JpaQuery) {
    JpaQuery<T> jQuery = (JpaQuery<T>) query;
    jQuery.setHint(QueryHints.RESULT_SET_TYPE, ResultSetType.ForwardOnly)
       .setHint(QueryHints.SCROLLABLE_CURSOR, true);

    final Cursor cursor = jQuery.getResultCursor();
    return new Iterable<T>()
    {     
      @SuppressWarnings("unchecked")
      @Override
      public Iterator<T> iterator()
      {
        return cursor;
      }
    }; 
   }
  return query.getResultList();  
}  

método de cierre

static void closeCursor(Iterable<?> list)
{
  if (list.iterator() instanceof Cursor)
    {
      ((Cursor) list.iterator()).close();
    }
}

6
Buen objeto jQuery
usr-local-ΕΨΗΕΛΩΝ

Probé su código pero aún obtengo OOM: parece que todos los objetos T (y todos los objetos de tabla unidos referidos desde T) nunca son GC. La creación de perfiles muestra que se hace referencia a ellos desde "tabla" en org.eclipse.persistence.internal.sessions.RepeatableWriteUnitOfWork junto con org.eclipse.persistence.internal.identitymaps.CacheKey. Miré en el caché y mis configuraciones son todas predeterminadas (Desactivar selectivo, Débil con subcaché suave, Tamaño de caché 100, Drop Invalidate). Examinaré las sesiones de desactivación y veré si ayuda. Por cierto, simplemente iteraré sobre el cursor de retorno usando "para (T o: resultados)".
Edi Bice

Badum tssssssss
dctremblay

5

Depende del tipo de operación que tenga que realizar. ¿Por qué recorre más de un millón de filas? ¿Está actualizando algo en modo por lotes? ¿Vas a mostrar todos los registros a un cliente? ¿Está 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 sus 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 útil Query.executeUpdate(). Opcionalmente, puede ejecutar la actualización en modo asíncrono utilizando un Bean o Administrador de trabajo basado en mensajes.

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

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


Simplemente, necesito hacer algo "para cada" fila. Seguramente este es un caso de uso común. En el caso específico en el que estoy trabajando ahora, necesito consultar un servicio web externo que está totalmente fuera de mi base de datos, usando una identificación (el PK) de cada fila. Los resultados no se muestran en ningún navegador web del cliente, por lo que no hay una interfaz de usuario de la que hablar. Es un trabajo por lotes, en otras palabras.
George Armhold

Si "necesita" la identificación de impresión para cada fila, no hay otra manera de obtener cada fila, obtener la identificación e imprimir. La mejor solución depende de lo que necesite hacer.
Dainius

@Caffeine Coma, si solo necesita la identificación de cada fila, entonces la mayor mejora probablemente vendría de solo obtener esa columna, como SELECT m.id FROM Model my luego iterar sobre una List <Integer>.
Jörn Horstmann

1
@ Jörn Horstmann- si hay millones de filas, ¿realmente importará? Mi punto es que una ArrayList con millones de objetos (por pequeños que sean) no va a ser buena para el montón de JVM.
George Armhold

@Dainius: mi pregunta es realmente: "¿cómo puedo iterar sobre cada fila, sin tener toda la ArrayList en la memoria?" En otras palabras, me gustaría una interfaz para extraer N a la vez, donde N es significativamente menor que 1 millón. :-)
George Armhold

5

No hay "correcto" qué hacer esto, esto no es lo que JPA o JDO o cualquier otro ORM está destinado a hacer, JDBC directo será su mejor alternativa, ya que puede configurarlo para recuperar una pequeña cantidad de filas en un tiempo y vaciarlos 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 hacer 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. A 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 tiempo razonable debido a la sobrecarga de creación de instancias de objetos, simple y llanamente.

Utilice la herramienta adecuada. El JDBC directo y los procedimientos almacenados definitivamente tienen un lugar en 2011, especialmente en lo que son mejores en hacer frente a estos marcos ORM.

Extraer un millón de cualquier cosa, incluso en un simple, List<Integer>no será muy eficiente, independientemente de cómo lo hagas. La forma correcta de hacer lo que está pidiendo es simple SELECT id FROM table, establecido en SERVER SIDE(dependiente del proveedor) y el cursor enFORWARD_ONLY READ-ONLY y iterar sobre eso.

Si realmente está extrayendo millones de identificaciones para procesar llamando a algún servidor web con cada una, también tendrá que realizar un procesamiento simultáneo para que esto se ejecute en un período de tiempo razonable. Tirando con un cursor JDBC y colocando algunos de ellos a la vez en una ConcurrentLinkedQueue y tener un pequeño grupo de subprocesos (# CPU / Cores + 1) extraerlos y procesarlos es la única forma de completar su tarea en una máquina con cualquier " "cantidad normal" de RAM, dado que ya se está quedando sin memoria.

Vea esta respuesta también.


1
¿Entonces está diciendo que ninguna empresa necesita visitar cada fila de la tabla de usuarios? ¿Sus programadores simplemente tiran a Hibernate por la ventana cuando llega el momento de hacer esto? " No hay manera de procesar cientos de miles de filas " - en mi pregunta señalé setFirstResult / setMaxResult, por lo que claramente no es una forma. Pregunto si hay uno mejor.
George Armhold

"Extraer un millón de cualquier cosa, incluso en una simple Lista <Intero>, no va a ser muy eficiente, independientemente de cómo lo hagas". Ese es exactamente mi punto. Estoy preguntando cómo no crear la lista gigante, sino iterar sobre un conjunto de resultados.
George Armhold

Use una declaración de selección JDBC simple y recta con un FORWARD_ONLY READ_ONLY con un cursor SERVER_SIDE como sugerí en mi respuesta. Cómo hacer que JDBC use un cursor SERVER_SIDE depende del controlador de la base de datos.

1
Estoy totalmente de acuerdo con la respuesta. La mejor solución depende del problema. Si el problema es cargar algunas entidades fácilmente, JPA es bueno. Si el problema es utilizar grandes cantidades de datos de manera eficiente, JDBC directo es mejor.
Extraneon

4
Escanear millones de registros es común por varias razones, por ejemplo, indexarlos en un motor de búsqueda. Y aunque estoy de acuerdo en que JDBC es normalmente una ruta más directa, a veces entras en un proyecto que ya tiene una lógica de negocios muy compleja incluida en una capa de Hibernación. Si lo omite y va a JDBC, omite la lógica empresarial, que a veces no es trivial de volver a implementar y mantener. Cuando las personas publican preguntas sobre casos de uso atípicos, a menudo saben que es un poco extraño, pero pueden estar heredando algo en lugar de construir desde cero, y tal vez no puedan revelar detalles.
Mark Bennett

4

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

Otro comentario: de todos modos, debe hacer esto en trozos, especialmente si modifica registros, de lo contrario, el segmento de retroceso en la base de datos crecerá.

Cuando se trata de establecer la estrategia firstResult / maxRows, será MUY MUY lento para los resultados lejos de la cima.

También tenga en cuenta que la base de datos probablemente esté operando en aislamiento de lectura confirmada , por lo que para evitar las lecturas fantasma cargue identificadores y luego cargue las entidades una por una (o 10 por 10 o lo que sea).


Hola @Marcin, ¿puede usted o cualquier otra persona proporcionar un enlace al código de ejemplo aplicando este enfoque escalonado fragmentado y de identificación primero, preferiblemente utilizando secuencias de Java8?
krevelen

2

Me sorprendió ver que el uso de procedimientos almacenados no era más prominente en las respuestas aquí. En el pasado, cuando tenía que hacer algo como esto, creaba un procedimiento almacenado que procesa datos en pequeños fragmentos, luego duerme un poco y luego continúa. El motivo de la suspensión es no abrumar la base de datos que presumiblemente también se utiliza para tipos de consultas más en tiempo real, como estar conectado a un sitio web. Si no hay nadie más usando la base de datos, puede omitir la suspensión. Si necesita asegurarse de procesar cada registro una vez y solo una vez, deberá crear una tabla (o campo) adicional para almacenar los registros que ha procesado a fin de ser resistente en los reinicios.

Los ahorros de rendimiento aquí son significativos, posiblemente órdenes de magnitud más rápidos que cualquier cosa que pueda hacer en JPA / Hibernate / AppServer land, y su servidor de base de datos probablemente tendrá su propio tipo de mecanismo de cursor del lado del servidor para procesar grandes conjuntos de resultados de manera eficiente. Los ahorros de rendimiento provienen de no tener que enviar los datos desde el servidor de la base de datos al servidor de aplicaciones, donde procesa los datos y luego los devuelve.

Existen algunas desventajas importantes en el uso de procedimientos almacenados que pueden descartarlo por completo, pero si tiene esa habilidad en su caja de herramientas personal y puede usarla en este tipo de situación, puede eliminar este tipo de cosas con bastante rapidez. .


1
-2 votos en contra: ¿el próximo votante en contra, defenderá su voto en contra?
Danger

1
Pensé lo mismo mientras leía estos. La pregunta indica un trabajo por lotes de gran volumen sin interfaz de usuario. Suponiendo que no necesita recursos específicos del servidor de aplicaciones, ¿por qué utilizar un servidor de aplicaciones? El procedimiento almacenado sería mucho más eficiente.
jdessey

@jdessey Dependiendo de la situación, digamos que tenemos una función de importación en la que al importar debería hacer algo con alguna otra parte del sistema, por ejemplo, agregar filas a otra tabla en función de algunas reglas comerciales que ya se han codificado como EJB. Entonces, ejecutar en un servidor de aplicaciones tendría más sentido, a menos que pueda hacer que EJB se ejecute en un modo integrado.
Archimedes Trajano

1

Para ampliar la respuesta de @Tomasz Nurkiewicz. Tienes acceso al DataSourceque a su vez puede proporcionarte 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 JPA para algunas operaciones por lotes grandes específicas como la importación / exportación, sin embargo, aún tiene acceso al administrador de la entidad para otras operaciones JPA si lo necesita.


0

Utilice PaginationConcept para recuperar el resultado


4
La paginación es muy buena para las GUI. Pero para procesar grandes cantidades de datos, ScrollableResultSet se inventó hace mucho tiempo. Simplemente no está en JPA.
Extraneon

0

Yo mismo me lo he preguntado. 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é sin usar mi iterador de fragmentos (por lo que podría no ser tan probado). Por cierto, necesitarás colecciones de Google si quieres usarlo.


Con respecto a "qué tipo de procesamiento está haciendo para cada fila", si el número de filas es de millones, sospecho que incluso un objeto simple con solo una columna de identificación va a causar problemas. Yo también pensé en escribir mi propio iterador que envolviera setFirstResult / setMaxResult, pero pensé que esto debe ser un problema común (¡y con suerte resuelto!).
George Armhold

@Caffeine Coma Publiqué mi iterador, probablemente podrías hacer un poco más de JPA adaptándose a él. Dime si te ayuda. Terminé sin usar (hice un findAll).
Adam Gent

0

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

  1. Usar sesión sin estado con scroll ()
  2. Utilice session.clear () después de cada iteración. Cuando sea necesario adjuntar otras entidades, cárguelas en una sesión separada. efectivamente, la primera sesión emula la sesión sin estado, pero conservando todas las características de una sesión con estado, hasta que los objetos se separan.
  3. Use iterate () o list () pero obtenga solo los identificadores 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 () también conocido como Session.evict ();

0

Aquí hay un ejemplo de JPA simple y directo (en Kotlin) que muestra cómo puede paginar sobre un conjunto de resultados arbitrariamente grande, leyendo fragmentos de 100 elementos a la vez, sin usar un cursor (cada cursor consume recursos en la base de datos). Utiliza la paginación del conjunto de claves.

Consulte https://use-the-index-luke.com/no-offset para conocer el concepto de paginación del conjunto de claves y https://www.citusdata.com/blog/2016/03/30/five-ways-to- paginate / para una comparación de diferentes formas de paginar junto con sus inconvenientes.

/*
create table my_table(
  id int primary key, -- index will be created
  my_column varchar
)
*/

fun keysetPaginationExample() {
    var lastId = Integer.MIN_VALUE
    do {

        val someItems =
        myRepository.findTop100ByMyTableIdAfterOrderByMyTableId(lastId)

        if (someItems.isEmpty()) break

        lastId = someItems.last().myTableId

        for (item in someItems) {
          process(item)
        }

    } while (true)
}

0

Un ejemplo con JPA y NativeQuery recuperando cada vez los elementos de tamaño usando compensaciones

public List<X> getXByFetching(int fetchSize) {
        int totalX = getTotalRows(Entity);
        List<X> result = new ArrayList<>();
        for (int offset = 0; offset < totalX; offset = offset + fetchSize) {
            EntityManager entityManager = getEntityManager();
            String sql = getSqlSelect(Entity) + " OFFSET " + offset + " ROWS";
            Query query = entityManager.createNativeQuery(sql, X.class);
            query.setMaxResults(fetchSize);
            result.addAll(query.getResultList());
            entityManager.flush();
            entityManager.clear();
        return result;
    }
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.