web-dev-qa-db-de.com

JPA: Was ist das richtige Muster, um große Ergebnismengen zu durchlaufen?

Nehmen wir an, ich habe eine Tabelle mit Millionen Zeilen. Wie kann ich mit JPA eine Abfrage mit dieser Tabelle wiederholen, so dass ich habe nicht alle eine In-Memory-Liste mit Millionen von Objekten?

Ich vermute zum Beispiel, dass das Folgende in die Luft geht, wenn der Tisch groß ist:

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

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

Ist die Paginierung (Schleifen und manuelle Aktualisierung von setFirstResult()/setMaxResult()) die beste Lösung?

Bearbeiten : Der primäre Anwendungsfall, den ich anvisiere, ist eine Art Stapeljob. Es ist in Ordnung, wenn der Lauf lange dauert. Es ist kein Webclient beteiligt. Ich muss nur für jede Reihe etwas tun, eines (oder ein kleines N) auf einmal. Ich versuche nur zu vermeiden, dass sie alle gleichzeitig in Erinnerung bleiben.

105
George Armhold

Seite 537 von Java Persistence with Hibernate gibt eine Lösung mit ScrollableResults, aber leider nur für Hibernate. 

Es scheint also, dass die Verwendung von setFirstResult/setMaxResults und manueller Iteration wirklich notwendig ist. Hier ist meine Lösung mit JPA:

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

dann verwenden Sie es wie folgt:

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

Ich habe die hier vorgestellten Antworten ausprobiert, aber JBoss 5.1 + MySQL Connector/J 5.1.15 + Hibernate 3.3.2 funktionierte nicht mit diesen. Wir haben gerade von JBoss 4.x zu JBoss 5.1 gewechselt, also haben wir uns erst einmal daran gehalten, und daher ist der letzte Hibernate 3.3.2.

Das Hinzufügen einiger zusätzlicher Parameter hat den Job erledigt, und Code wie dieser läuft ohne 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();

Die entscheidenden Linien sind die Abfrageparameter zwischen createQuery und scroll. Ohne sie versucht der "Scroll" -Aufruf, alles in den Speicher zu laden, und wird entweder nie beendet oder läuft nach OutOfMemoryError.

32
Zds

In reiner JPA ist dies nicht wirklich möglich, jedoch bietet Hibernate Unterstützung für zustandslose Sitzungen und scrollbare Ergebnissätze.

Wir verarbeiten Milliarden Zeilen regelmäßig mit seiner Hilfe.

Hier ist ein Link zur Dokumentation: http://docs.jboss.org/hibernate/core/3.3/reference/en/html/batch.html#batch-statelesssession

28
Cyberax

Um ehrlich zu sein, würde ich vorschlagen, JPA zu verlassen und bei JDBC zu bleiben (aber auf jeden Fall JdbcTemplate Support-Klasse oder ähnliches). JPA (und andere ORM-Anbieter/Spezifikationen) sind nicht für die Bearbeitung vieler Objekte in einer Transaktion konzipiert, da sie davon ausgehen, dass alles, was geladen wird, im Cache der ersten Ebene verbleiben muss (daher die Notwendigkeit von clear() in JPA).

Außerdem empfehle ich eine eher untergeordnete Lösung, da der Overhead von ORM (Reflexion ist nur eine Spitze eines Eisbergs) so bedeutend sein könnte, dass das Durchlaufen von Ebene ResultSet auch bei Verwendung einer leichten Unterstützung wie der oben genannten JdbcTemplate schneller ist.

JPA ist einfach nicht dafür ausgelegt, Operationen an einer großen Anzahl von Entitäten durchzuführen. Sie können mit flush()/clear() spielen, um OutOfMemoryError zu vermeiden, dies aber noch einmal zu überdenken. Sie verdienen sehr wenig, wenn Sie den Preis für den enormen Ressourcenverbrauch bezahlen.

17

Wenn Sie EclipseLink I verwenden, verwenden Sie diese Methode, um das Ergebnis als Iterable zu erhalten

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

methode schließen

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

Es hängt von der Art der Operation ab, die Sie ausführen müssen. Warum schleifen Sie über eine Million Reihen? Aktualisieren Sie etwas im Stapelmodus? Wollen Sie einem Kunden alle Datensätze anzeigen? Berechnen Sie Statistiken zu den abgerufenen Entitäten?

Wenn Sie dem Client eine Million Datensätze anzeigen möchten, überprüfen Sie bitte Ihre Benutzeroberfläche. In diesem Fall paginiert die geeignete Lösung Ihre Ergebnisse und verwendet setFirstResult() und setMaxResult().

Wenn Sie ein Update mit einer großen Anzahl von Datensätzen gestartet haben, halten Sie das Update einfach und verwenden Sie Query.executeUpdate(). Optional können Sie das Update im asynchronen Modus mit einem Message-Driven Bean oder einem Work Manager ausführen.

Wenn Sie Statistiken für die abgerufenen Entitäten berechnen, können Sie die in der JPA-Spezifikation definierten Gruppierungsfunktionen nutzen.

Für jeden anderen Fall bitte genauer sein :)

5
frm

Es gibt kein "richtiges" Vorgehen, dies ist nicht das, was JPA oder JDO oder ein anderes ORM beabsichtigt, sondern gerade JDBC ist die beste Alternative, da Sie es so konfigurieren können, dass eine kleine Anzahl von Zeilen zurückgegeben wird eine Zeit und leeren sie, wenn sie verwendet werden, deshalb gibt es serverseitige Cursor.

ORM-Tools sind nicht für die Massenverarbeitung konzipiert. Sie dienen dazu, Objekte zu manipulieren und zu versuchen, das RDBMS so zu gestalten, dass die Daten so transparent wie möglich sind. Die meisten Fehler fallen im transparenten Teil zumindest teilweise aus. Bei dieser Skalierung gibt es keine Möglichkeit, Hunderttausende von Zeilen (Objekten) zu verarbeiten, viel weniger Millionen mit einem beliebigen ORM, und lassen es aufgrund des Objekt-Overheads der Objektinstanzierung in einer vernünftigen Zeitspanne ausführen. 

Verwenden Sie das entsprechende Werkzeug. Gerade JDBC und gespeicherte Prozeduren haben definitiv einen Platz im Jahr 2011, vor allem, wenn sie besser als diese ORM-Frameworks sind.

Eine Million von etwas, sogar in einen einfachen List<Integer>, zu ziehen, wird unabhängig von Ihrer Arbeitsweise nicht sehr effizient sein. Der richtige Weg, um das zu tun, was Sie verlangen, ist ein einfacher SELECT id FROM table, der auf SERVER SIDE (herstellerabhängig) und den Cursor auf FORWARD_ONLY READ-ONLY gesetzt ist, und darüber iterieren.

Wenn Sie wirklich Millionen von IDs in die Verarbeitung ziehen, indem Sie mit jedem von ihnen einen Webserver aufrufen, müssen Sie auch eine gleichzeitige Verarbeitung durchführen, damit dies in einer angemessenen Zeitspanne ausgeführt werden kann. Wenn Sie mit einem JDBC-Cursor ziehen und jeweils ein paar davon in ConcurrentLinkedQueue platzieren und einen kleinen Pool von Threads (# CPU/Cores + 1) ziehen und verarbeiten, ist dies die einzige Möglichkeit, Ihre Aufgabe auf einem Maschine mit beliebiger "normaler" RAM-Kapazität, vorausgesetzt, der Speicher ist knapp.

Siehe auch answer .

4
user177800

Sie können einen anderen "Trick" verwenden. Laden Sie nur eine Sammlung von Bezeichnern der Entitäten, an denen Sie interessiert sind. Angenommen, der Bezeichner ist vom Typ long = 8Byte, dann erzeugt eine Liste dieser Bezeichner eine Menge von 8 MB. Wenn es sich um einen Batch-Prozess handelt (jeweils eine Instanz), ist es erträglich. Dann iterieren Sie einfach und erledigen Sie die Arbeit.

Noch eine Bemerkung - Sie sollten dies auf jeden Fall in Abschnitten tun - besonders, wenn Sie Datensätze ändern. Andernfalls wird rollback segment in der Datenbank größer.

Wenn es darum geht, die firstResult/maxRows-Strategie festzulegen, wird es SEHR SEHR langsam sein, wenn die Ergebnisse weit oben liegen.

Berücksichtigen Sie auch, dass die Datenbank wahrscheinlich in read commited isolation arbeitet. Um Phantom Reads zu vermeiden, laden Sie die Bezeichner und laden Sie die Entitäten einzeln (oder 10 x 10 oder was auch immer).

3
Marcin Cinik

Auf @Tomasz Nurkiewiczs Antwort erweitern. Sie haben Zugriff auf DataSource, wodurch Sie eine Verbindung herstellen können

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

In deinem Code hast du 

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

Auf diese Weise können Sie JPA für bestimmte große Stapelvorgänge wie Import/Export umgehen. Sie haben jedoch weiterhin Zugriff auf den Entitätsmanager für andere JPA-Vorgänge, falls Sie dies benötigen.

1

Ich war überrascht zu sehen, dass die Verwendung von gespeicherten Prozeduren in den Antworten hier nicht wichtiger war. Wenn ich in der Vergangenheit so etwas tun musste, erstelle ich eine gespeicherte Prozedur, die Daten in kleinen Abschnitten verarbeitet, dann ein wenig schläft und dann fortfährt. Der Grund für das Einschlafen ist, die Datenbank nicht zu überfordern, die vermutlich auch für mehr Echtzeittypen von Abfragen verwendet wird, z. B. für die Verbindung zu einer Website. Wenn niemand die Datenbank verwendet, können Sie den Ruhezustand auslassen. Wenn Sie sicherstellen müssen, dass Sie jeden Datensatz einmal und nur einmal verarbeiten, müssen Sie eine zusätzliche Tabelle (oder ein zusätzliches Feld) erstellen, in der gespeichert wird, welche Datensätze Sie verarbeitet haben, um für Neustarts geeignet zu sein.

Die Leistungseinsparungen sind beträchtlich, möglicherweise um Größenordnungen schneller als alles, was Sie in JPA/Hibernate/AppServer landen könnten, und Ihr Datenbankserver verfügt höchstwahrscheinlich über einen eigenen serverseitigen Cursor-Mechanismus für die effiziente Verarbeitung großer Ergebnissätze. Die Leistungseinsparungen ergeben sich daraus, dass die Daten nicht vom Datenbankserver an den Anwendungsserver gesendet werden müssen, wo Sie die Daten verarbeiten und anschließend zurücksenden.

Die Verwendung von gespeicherten Prozeduren hat einige bedeutende Nachteile, die dies für Sie vollständig ausschließen können. Wenn Sie jedoch diese Fähigkeit in Ihrer persönlichen Toolbox haben und in einer solchen Situation einsetzen können, können Sie diese Art von Dingen ziemlich schnell ausschalten .

1
Danger

Mit Winterschlaf gibt es 4 verschiedene Möglichkeiten, um das zu erreichen, was Sie möchten. Jeder hat Design-Kompromisse, Einschränkungen und Konsequenzen. Ich schlage vor, jedes zu erkunden und zu entscheiden, welches für Ihre Situation geeignet ist.

  1. Stateless-Session mit scroll () verwenden
  2. Verwenden Sie nach jeder Iteration session.clear (). Wenn andere Entitäten verbunden werden müssen, laden Sie sie in einer separaten Sitzung. effektiv emuliert die erste Sitzung die zustandslose Sitzung, behält jedoch alle Funktionen einer statusbehafteten Sitzung bei, bis die Objekte getrennt werden.
  3. Verwenden Sie iterate () oder list (), erhalten Sie jedoch nur IDs in der ersten Abfrage. Führen Sie dann in jeder Iteration eine separate Sitzung aus.
  4. Verwenden Sie Query.iterate () mit EntityManager.detach () aka Session.evict ();
0
Larry Chu

Verwenden Sie Pagination Concept zum Abrufen des Ergebnisses 

0
Dead Programmer

Ich habe mich selbst gefragt. Es scheint wichtig zu sein:

  • wie groß Ihr Datensatz ist (Zeilen)
  • welche JPA-Implementierung Sie verwenden 
  • welche Art von Verarbeitung Sie für jede Zeile ausführen.

Ich habe einen Iterator geschrieben, um den Austausch beider Ansätze zu vereinfachen (findAll vs findEntries).

Ich empfehle, dass du beide probierst.

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

}

Am Ende habe ich meinen Chunk-Iterator nicht verwendet (daher ist er möglicherweise nicht so getestet). Übrigens werden Sie Google-Sammlungen benötigen, wenn Sie diese verwenden möchten.

0
Adam Gent