JPA: qual è il modello corretto per l'iterazione su set di risultati di grandi dimensioni?


114

Diciamo che ho una tabella con milioni di righe. Utilizzando JPA, qual è il modo corretto per iterare su una query su quella tabella, in modo tale da non avere tutto un elenco in memoria con milioni di oggetti?

Ad esempio, sospetto che il seguente esploderà se la tabella è grande:

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

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

L'impaginazione (loop e aggiornamento manuale setFirstResult()/ setMaxResult()) è davvero la soluzione migliore?

Modifica : il caso d'uso principale a cui mi rivolgo è una sorta di lavoro in batch. Va bene se ci vuole molto tempo per funzionare. Non è coinvolto alcun client web; Ho solo bisogno di "fare qualcosa" per ogni riga, una (o qualche piccola N) alla volta. Sto solo cercando di evitare di averli tutti in memoria allo stesso tempo.


Quale database e driver JDBC stai utilizzando?

Risposte:


55

La pagina 537 di Java Persistence with Hibernate fornisce una soluzione usando ScrollableResults, ma purtroppo è solo per Hibernate.

Quindi sembra che l'uso di setFirstResult/ setMaxResultse l'iterazione manuale sia davvero necessario. Ecco la mia soluzione utilizzando JPA:

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

quindi, usalo in questo modo:

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
Penso che l'esempio non sia sicuro se ci sono nuovi inserti durante il processo batch. L'utente deve ordinare in base a una colonna in cui è sicuro che i dati appena inseriti saranno alla fine dell'elenco dei risultati.
Balazs Zsoldos

quando la pagina corrente è l'ultima e ha meno di 100 elementi, il controllo size() == 100salterà invece una query aggiuntiva che restituisce un elenco vuoto
cdalxndr

38

Ho provato le risposte presentate qui, ma JBoss 5.1 + MySQL Connector / J 5.1.15 + Hibernate 3.3.2 non funzionavano con quelli. Abbiamo appena migrato da JBoss 4.x a JBoss 5.1, quindi per ora ci siamo fermati, e quindi l'ultimo Hibernate che possiamo usare è 3.3.2.

L'aggiunta di un paio di parametri extra ha fatto il lavoro e il codice come questo viene eseguito senza OOME:

        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();

Le righe cruciali sono i parametri di query tra createQuery e scroll. Senza di essi la chiamata "scroll" tenta di caricare tutto in memoria e non finisce mai o viene eseguita su OutOfMemoryError.


2
Ciao Zds, il tuo caso d'uso della scansione di milioni di righe è certamente comune per me e GRAZIE per aver pubblicato il codice finale. Nel mio caso sto inserendo i record in Solr, per indicizzarli per la ricerca full-text. E, a causa delle regole aziendali in cui non entrerò, devo passare tramite Hibernate, anziché utilizzare solo JDBC o i moduli integrati di Solr.
Mark Bennett

Felice di aiutare :-). Abbiamo anche a che fare con set di dati di grandi dimensioni, in questo caso consentendo all'utente di interrogare tutti i nomi di strade all'interno della stessa città / contea, o talvolta anche stato, quindi la creazione di indici richiede la lettura di molti dati.
Zds

Appare con MySQL, devi davvero passare attraverso tutti quei cerchi: stackoverflow.com/a/20900045/32453 (altri DB potrebbero essere meno rigorosi che immagino ...)
rogerdpack

32

Non puoi farlo in JPA diretto, tuttavia Hibernate ha il supporto per sessioni senza stato e set di risultati scorrevoli.

Con il suo aiuto elaboriamo regolarmente miliardi di righe.

Ecco un collegamento alla documentazione: http://docs.jboss.org/hibernate/core/3.3/reference/en/html/batch.html#batch-statelesssession


17
Grazie. Buono a sapersi che qualcuno sta facendo miliardi di righe attraverso Hibernate. Alcune persone qui affermano che è impossibile. :-)
George Armhold

2
È possibile aggiungere un esempio anche qui? Presumo sia simile all'esempio di Zds?
rogerdpack

19

Ad essere onesto, suggerirei di lasciare JPA e restare con JDBC (ma certamente usando JdbcTemplateclassi di supporto o simili). JPA (e altri fornitori / specifiche ORM) non è progettato per operare su molti oggetti all'interno di una transazione poiché presumono che tutto ciò che viene caricato debba rimanere nella cache di primo livello (da qui la necessità di clear()JPA).

Inoltre sto raccomandando una soluzione di livello più basso perché il sovraccarico di ORM (il riflesso è solo la punta di un iceberg) potrebbe essere così significativo, che l'iterazione sulla pianura ResultSet, anche utilizzando un supporto leggero come menzionato, JdbcTemplatesarà molto più veloce.

JPA semplicemente non è progettato per eseguire operazioni su una grande quantità di entità. Potresti giocare con flush()/ clear()per evitare OutOfMemoryError, ma considera questo ancora una volta. Guadagni molto poco pagando il prezzo di un enorme consumo di risorse.


Il vantaggio di JPA non è solo l'essere agnostico del database, ma la possibilità di non utilizzare nemmeno un database tradizionale (NoSQL). Non è difficile eseguire il flush / clear di tanto in tanto e di solito le operazioni batch vengono eseguite di rado.
Adam Gent,

1
Ciao Thomasz. Ho molte ragioni per lamentarmi di JPA / Hibernate, ma rispettosamente, dubito davvero che "non siano progettati per operare su molti oggetti". Sospetto di dover solo imparare il modello corretto per questo caso d'uso.
George Armhold

4
Bene, posso pensare solo a due schemi: impaginazioni (menzionate più volte) e flush()/ clear(). Il primo è IMHO non progettato ai fini dell'elaborazione batch, mentre l'uso della sequenza flush () / clear () odora di astrazione che perde .
Tomasz Nurkiewicz

Sì, era una combinazione di impaginazione e colore / chiaro come hai detto. Grazie!
George Armhold

7

Se utilizzi EclipseLink, utilizza questo metodo per ottenere il risultato come Iterabile

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

Chiudi Metodo

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

6
Nizza jQuery oggetto
usr-local-ΕΨΗΕΛΩΝ

Ho provato il tuo codice ma ottengo ancora OOM: sembra che tutti gli oggetti T (e tutti gli oggetti tabella uniti a cui fa riferimento T) non siano mai GC. La creazione di profili mostra loro riferimenti dalla "tabella" in org.eclipse.persistence.internal.sessions.RepeatableWriteUnitOfWork insieme a org.eclipse.persistence.internal.identitymaps.CacheKey. Ho esaminato la cache e le mie impostazioni sono tutte predefinite (Disable Selective, Weak with Soft Subcache, Cache Size 100, Drop Invalidate). Cercherò di disabilitare le sessioni e vedrò se aiuta. BTW semplicemente iterare sul cursore di ritorno usando "for (To: results)".
Edi Bice

Badum tssssssss
dctremblay

5

Dipende dal tipo di operazione che devi fare. Perché stai ripetendo più di un milione di righe? Stai aggiornando qualcosa in modalità batch? Visualizzerai tutti i record a un cliente? Stai elaborando alcune statistiche sulle entità recuperate?

Se hai intenzione di visualizzare un milione di record al client, riconsidera la tua interfaccia utente. In questo caso, la soluzione appropriata è impaginare i risultati e utilizzare setFirstResult()e setMaxResult().

Se hai avviato un aggiornamento di una grande quantità di record, farai meglio a mantenerlo semplice e utilizzabile Query.executeUpdate(). Facoltativamente, è possibile eseguire l'aggiornamento in modalità asincrona utilizzando un Message-Driven Bean o Work Manager.

Se stai elaborando alcune statistiche sulle entità recuperate, puoi trarre vantaggio dalle funzioni di raggruppamento definite dalla specifica JPA.

Per qualsiasi altro caso, sii più specifico :)


Molto semplicemente, devo fare qualcosa "per ogni" riga. Sicuramente questo è un caso d'uso comune. Nel caso specifico su cui sto lavorando ora, ho bisogno di interrogare un servizio web esterno che è totalmente al di fuori del mio database, utilizzando un id (il PK) da ogni riga. I risultati non vengono visualizzati su alcun browser Web client, quindi non esiste un'interfaccia utente di cui parlare. È un lavoro in batch, in altre parole.
George Armhold,

Se "hai bisogno" di print id per ogni riga, non c'è altro modo come ottenere ogni riga, ottenere id e stampare. La migliore soluzione dipende da cosa devi fare.
Dainius

@Caffeine Coma, se hai solo bisogno dell'id di ogni riga, il miglioramento più grande verrebbe probabilmente dal solo recupero di quella colonna, come SELECT m.id FROM Model me poi iterando su un List <Integer>.
Jörn Horstmann

1
@ Jörn Horstmann- se ci sono milioni di righe, sarà davvero importante? Il punto è che un ArrayList con milioni di oggetti (per quanto piccoli) non andrà bene per l'heap JVM.
George Armhold,

@Dainius: la mia domanda è davvero: "come posso iterare su ogni riga, senza avere l'intero ArrayList in memoria?" In altre parole, vorrei un'interfaccia per estrarre N alla volta, dove N è significativamente inferiore a 1 milione. :-)
George Armhold

5

Non esiste un "corretto" cosa fare, questo non è ciò che JPA o JDO o qualsiasi altro ORM è inteso fare, JDBC diretto sarà la tua migliore alternativa, poiché puoi configurarlo per ripristinare un piccolo numero di righe in una volta e svuotali man mano che vengono utilizzati, ecco perché esistono cursori lato server.

Gli strumenti ORM non sono progettati per l'elaborazione in blocco, sono progettati per consentire di manipolare oggetti e tentare di rendere l'RDBMS in cui sono archiviati i dati essere il più trasparente possibile, la maggior parte fallisce nella parte trasparente almeno in una certa misura. A questa scala, non è possibile elaborare centinaia di migliaia di righe (oggetti), tanto meno milioni con qualsiasi ORM e farlo eseguire in un ragionevole lasso di tempo a causa dell'overhead di istanziazione dell'oggetto, chiaro e semplice.

Usa lo strumento appropriato. Il JDBC diretto e le procedure memorizzate hanno sicuramente un posto nel 2011, specialmente in ciò che sono migliori nel fare rispetto a questi framework ORM.

Tirare un milione di qualsiasi cosa, anche in un semplice List<Integer>non sarà molto efficiente indipendentemente da come lo fai. Il modo corretto per fare ciò che stai chiedendo è un semplice SELECT id FROM table, impostare su SERVER SIDE(dipendente dal fornitore) e il cursore su FORWARD_ONLY READ-ONLYe iterare su quello.

Se stai davvero tirando milioni di ID da elaborare chiamando un server Web con ciascuno di essi, dovrai eseguire anche un'elaborazione simultanea affinché funzioni in un ragionevole lasso di tempo. Tirare con un cursore JDBC e posizionarne alcuni alla volta in un ConcurrentLinkedQueue e avere un piccolo pool di thread (# CPU / Core + 1) estrarli ed elaborarli è l'unico modo per completare l'attività su una macchina con qualsiasi " normale "quantità di RAM, dato che stai già esaurendo la memoria.

Vedi anche questa risposta .


1
Quindi stai dicendo che nessuna azienda ha mai bisogno di visitare ogni riga della tabella degli utenti? I loro programmatori lanciano Hibernate dalla finestra quando arriva il momento di farlo? " Non v'è alcun modo per centinaia di processo di migliaia di righe " - nella mia interrogazione ho sottolineato setFirstResult / setMaxResult, così chiaramente ci sia un modo. Chiedo se ce n'è uno migliore.
George Armhold

"Tirare un milione di qualsiasi cosa, anche in un semplice List <Integer> non sarà molto efficiente indipendentemente da come lo fai." Questo è esattamente il mio punto. Chiedo come non creare l'elenco gigante, ma piuttosto iterare su un set di risultati.
George Armhold,

Usa una semplice istruzione JDBC select con un FORWARD_ONLY READ_ONLY con un cursore SERVER_SIDE come ho suggerito nella mia risposta. Il modo in cui JDBC utilizza un cursore SERVER_SIDE dipende dal driver del database.

1
Sono pienamente d'accordo con la risposta. La soluzione migliore dipende dal problema. Se il problema sta caricando facilmente alcune entità, JPA va bene. Se il problema sta utilizzando enormi quantità di dati in modo efficiente, JDBC diretto è migliore.
extraneon

4
La scansione di milioni di record è comune per una serie di motivi, ad esempio indicizzandoli in un motore di ricerca. E sebbene io sia d'accordo sul fatto che JDBC sia normalmente un percorso più diretto, a volte entri in un progetto che ha già una logica aziendale molto complessa raggruppata in un livello Hibernate. Se lo ignori e vai a JDBC, ignori la logica aziendale, che a volte non è banale da reimplementare e mantenere. Quando le persone pubblicano domande su casi d'uso atipici, spesso sanno che è un po 'strano, ma potrebbero ereditare qualcosa invece di costruire da zero e forse non possono rivelare i dettagli.
Mark Bennett

4

Puoi usare un altro "trucco". Carica solo la raccolta di identificatori delle entità a cui sei interessato. Supponiamo che l'identificatore sia di tipo long = 8bytes, quindi 10 ^ 6 un elenco di tali identificatori fa circa 8Mb. Se si tratta di un processo batch (un'istanza alla volta), è sopportabile. Quindi iterare e fare il lavoro.

Un'altra osservazione: dovresti comunque farlo in blocchi, specialmente se modifichi i record, altrimenti il segmento di rollback nel database crescerà.

Quando si tratta di impostare la strategia firstResult / maxRows, sarà MOLTO MOLTO lento per risultati lontani dalla cima.

Tieni anche in considerazione che il database sta probabilmente operando in isolamento read commited , in modo da evitare letture fantasma caricare gli identificatori e quindi caricare le entità una per una (o 10 per 10 o altro).


Ciao @ Marcin, tu o qualcun altro potete fornire un collegamento al codice di esempio applicando questo approccio graduale a blocchi e id-first, preferibilmente utilizzando flussi Java8?
krevelen

2

Sono stato sorpreso di vedere che l'uso di stored procedure non era più prominente nelle risposte qui. In passato, quando dovevo fare qualcosa di simile, creo una procedura memorizzata che elabora i dati in piccoli blocchi, quindi si ferma per un po ', quindi continua. Il motivo della sospensione è non sovraccaricare il database che presumibilmente viene utilizzato anche per tipi di query più in tempo reale, come la connessione a un sito web. Se non c'è nessun altro che utilizza il database, puoi lasciare fuori il sonno. Se è necessario assicurarsi di elaborare ogni record una sola volta, sarà necessario creare una tabella (o un campo) aggiuntivo per archiviare i record elaborati in modo da essere resilienti durante i riavvii.

I risparmi in termini di prestazioni qui sono significativi, forse ordini di grandezza più veloci di qualsiasi cosa tu possa fare in JPA / Hibernate / AppServer land, e molto probabilmente il tuo server di database avrà il suo tipo di cursore lato server per elaborare in modo efficiente set di risultati di grandi dimensioni. I risparmi sulle prestazioni derivano dal non dover inviare i dati dal server del database al server delle applicazioni, dove si elaborano i dati, per poi rispedirli.

Ci sono alcuni svantaggi significativi nell'utilizzo di stored procedure che potrebbero escluderlo completamente, ma se hai quell'abilità nella tua cassetta degli attrezzi personale e puoi usarla in questo tipo di situazione, puoi eliminare questo tipo di cose abbastanza rapidamente .


1
-2 voti negativi - il prossimo voto negativo difenderebbe il tuo voto negativo?
Pericolo

1
Ho pensato la stessa cosa leggendo questi. La domanda indica un lavoro batch ad alto volume senza interfaccia utente. Supponendo che non siano necessarie risorse specifiche del server app, perché utilizzare un server app? La stored procedure sarebbe molto più efficiente.
jdessey

@jdessey A seconda della situazione, diciamo di avere una funzione di importazione in cui durante l'importazione dovrebbe fare qualcosa con qualche altra parte del sistema, ad esempio aggiungere righe a un'altra tabella in base ad alcune regole di business che sono già state codificate come EJB. Quindi l'esecuzione in un server app avrebbe più senso, a meno che non sia possibile far funzionare l'EJB in una modalità incorporata.
Archimedes Trajano

1

Per espandere la risposta di @Tomasz Nurkiewicz. Hai accesso a ciò DataSourceche a sua volta può fornirti una connessione

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

Nel tuo codice hai

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

Ciò ti consentirà di bypassare JPA per alcune operazioni batch di grandi dimensioni specifiche come l'importazione / esportazione, tuttavia hai ancora accesso al gestore entità per altre operazioni JPA se ne hai bisogno.


0

Usa PaginationConcept per recuperare il risultato


4
L'impaginazione è molto buona per le GUI. Ma per elaborare enormi quantità di dati, ScrollableResultSet è stato inventato molto tempo fa. Semplicemente non è in JPA.
extraneon

0

Me lo sono chiesto io stesso. Sembra importare:

  • quanto è grande il tuo set di dati (righe)
  • quale implementazione JPA stai utilizzando
  • che tipo di elaborazione stai eseguendo per ogni riga.

Ho scritto un Iterator per semplificare lo scambio di entrambi gli approcci (findAll vs findEntries).

Ti consiglio di provare entrambi.

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

}

Ho finito per non usare il mio iteratore di blocchi (quindi potrebbe non essere testato). A proposito, avrai bisogno delle raccolte di Google se vuoi usarlo.


Per quanto riguarda "che tipo di elaborazione stai facendo per ogni riga" - se il numero di righe è nell'ordine dei milioni, sospetto che anche un semplice oggetto con solo una colonna id causerà problemi. Anch'io ho pensato di scrivere il mio Iterator che includesse setFirstResult / setMaxResult, ma ho pensato che questo dovesse essere un problema comune (e si spera risolto!).
George Armhold,

@Caffeine Coma Ho postato il mio Iterator, probabilmente potresti fare un po 'di JPA adattandoti ad esso. Dimmi se aiuta. Ho finito per non usare (ho fatto un findAll).
Adam Gent,

0

Con Hibernate ci sono 4 modi diversi per ottenere ciò che desideri. Ognuno ha compromessi, limitazioni e conseguenze di progettazione. Suggerisco di esplorarli tutti e di decidere quale è giusto per la tua situazione.

  1. Usa una sessione senza stato con scroll ()
  2. Usa session.clear () dopo ogni iterazione. Quando è necessario collegare altre entità, caricarle in una sessione separata. effettivamente la prima sessione emula la sessione senza stato, ma conserva tutte le caratteristiche di una sessione con stato, finché gli oggetti non vengono scollegati.
  3. Usa iterate () o list () ma ottieni solo gli id ​​nella prima query, quindi in una sessione separata in ogni iterazione, esegui session.load e chiudi la sessione alla fine dell'iterazione.
  4. Usa Query.iterate () con EntityManager.detach () aka Session.evict ();

0

Ecco un semplice e diretto esempio JPA (in Kotlin) che mostra come è possibile impaginare su un set di risultati arbitrariamente grande, leggendo blocchi di 100 elementi alla volta, senza utilizzare un cursore (ogni cursore consuma risorse sul database). Usa l'impaginazione del keyset.

Vedere https://use-the-index-luke.com/no-offset per il concetto di paginazione keyset e https://www.citusdata.com/blog/2016/03/30/five-ways-to- impagina / per un confronto tra diversi modi di impaginare insieme ai loro svantaggi.

/*
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 esempio con JPA e NativeQuery che recupera ogni volta la dimensione degli elementi utilizzando gli offset

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;
    }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.