Angenommen, ich habe eine Tabelle mit Millionen von Zeilen. Was ist bei Verwendung von JPA der richtige Weg, um eine Abfrage für diese Tabelle zu durchlaufen, sodass ich nicht alle eine speicherinterne Liste mit Millionen von Objekten habe?
Ich vermute zum Beispiel, dass Folgendes explodiert, 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 Paginierung (Schleifen und manuelles Aktualisieren setFirstResult()
/ setMaxResult()
) wirklich die beste Lösung?
Bearbeiten : Der primäre Anwendungsfall, auf den ich abziele, ist eine Art Stapeljob. Es ist in Ordnung, wenn die Ausführung lange dauert. Es ist kein Webclient beteiligt. Ich muss nur für jede Zeile "etwas" tun, eine (oder ein kleines N) nach der anderen. Ich versuche nur zu vermeiden, dass sie alle gleichzeitig im Gedächtnis bleiben.
Antworten:
Seite 537 von Java Persistence with Hibernate bietet 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:Verwenden Sie es dann folgendermaßen:
quelle
size() == 100
werden, wird stattdessen eine zusätzliche AbfrageIch habe die hier vorgestellten Antworten ausprobiert, aber JBoss 5.1 + MySQL Connector / J 5.1.15 + Hibernate 3.3.2 hat mit diesen nicht funktioniert. Wir sind gerade von JBoss 4.x auf JBoss 5.1 migriert, haben uns also vorerst daran gehalten, und daher ist der neueste Ruhezustand, den wir verwenden können, 3.3.2.
Das Hinzufügen einiger zusätzlicher Parameter hat den Job erledigt, und Code wie dieser wird ohne OOMEs ausgeführt:
Die entscheidenden Zeilen 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 in OutOfMemoryError ausgeführt.
quelle
In Straight JPA ist dies nicht wirklich möglich. Hibernate unterstützt jedoch zustandslose Sitzungen und scrollbare Ergebnismengen.
Mit seiner Hilfe verarbeiten wir routinemäßig Milliarden von Zeilen.
Hier ist ein Link zur Dokumentation: http://docs.jboss.org/hibernate/core/3.3/reference/en/html/batch.html#batch-statelesssession
quelle
Um ehrlich zu sein, würde ich vorschlagen, JPA zu verlassen und bei JDBC zu bleiben (aber sicherlich
JdbcTemplate
Support-Klassen oder ähnliches zu verwenden). JPA (und andere ORM-Anbieter / Spezifikationen) sind nicht für die Verarbeitung vieler Objekte innerhalb einer Transaktion ausgelegt, da davon ausgegangen wird, dass alles, was geladen wird, im Cache der ersten Ebene verbleiben sollte (daher ist diesclear()
in JPA erforderlich ).Außerdem empfehle ich eine Lösung auf niedrigerer Ebene, da der Overhead von ORM (Reflexion ist nur eine Spitze eines Eisbergs) möglicherweise so bedeutend ist, dass das Durchlaufen von Ebenen
ResultSet
, selbst wenn eine leichte Unterstützung wie erwähnt verwendetJdbcTemplate
wird, viel schneller ist.JPA ist einfach nicht dafür ausgelegt, Operationen an einer großen Anzahl von Entitäten auszuführen. Sie könnten mit
flush()
/ spielenclear()
, umOutOfMemoryError
dies zu vermeiden , aber denken Sie noch einmal darüber nach. Sie gewinnen sehr wenig, wenn Sie den Preis für einen enormen Ressourcenverbrauch bezahlen.quelle
flush()
/clear()
. Das erste ist meiner Meinung nach nicht für die Stapelverarbeitung konzipiert, während die Folge von Flush () / Clear () nach undichter Abstraktion riecht .Wenn Sie EclipseLink verwenden, verwende ich diese Methode, um das Ergebnis als Iterable zu erhalten
Methode schließen
quelle
Dies hängt von der Art der Operation ab, die Sie ausführen müssen. Warum schleifen Sie über eine Million Zeilen? Aktualisieren Sie etwas im Batch-Modus? Werden Sie einem Client alle Datensätze anzeigen? Berechnen Sie einige Statistiken zu den abgerufenen Entitäten?
Wenn Sie dem Client eine Million Datensätze anzeigen möchten, überdenken Sie bitte Ihre Benutzeroberfläche. In diesem Fall besteht die geeignete Lösung darin, Ihre Ergebnisse zu paginieren und
setFirstResult()
und zu verwendensetMaxResult()
.Wenn Sie ein Update für eine große Anzahl von Datensätzen gestartet haben, sollten Sie das Update einfach halten und verwenden
Query.executeUpdate()
. Optional können Sie das Update im asynchronen Modus mit einem Message-Driven Bean oa Work Manager ausführen.Wenn Sie Statistiken zu den abgerufenen Entitäten berechnen, können Sie die in der JPA-Spezifikation definierten Gruppierungsfunktionen nutzen.
Für jeden anderen Fall bitte genauer sein :)
quelle
SELECT m.id FROM Model m
und dann über eine Liste <Integer> zu iterieren.Es gibt kein "richtiges" Vorgehen, dies ist nicht das, was JPA oder JDO oder ein anderes ORM tun sollen. Gerade JDBC ist Ihre beste Alternative, da Sie es so konfigurieren können, dass eine kleine Anzahl von Zeilen zurückgebracht wird eine Zeit und leeren Sie sie, wie 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, in dem die Daten gespeichert sind, so transparent wie möglich zu gestalten. Die meisten Fehler treten zumindest teilweise im transparenten Bereich auf. In dieser Größenordnung gibt es keine Möglichkeit, Hunderttausende von Zeilen (Objekten), geschweige denn Millionen, mit einem ORM zu verarbeiten und es in angemessener Zeit ausführen zu lassen, da der Aufwand für die Objektinstanziierung schlicht und einfach ist.
Verwenden Sie das entsprechende Werkzeug. Straight JDBC und Stored Procedures haben 2011 definitiv einen Platz, insbesondere was sie im Vergleich zu diesen ORM-Frameworks besser können.
Eine Million von irgendetwas zu ziehen, selbst in eine einfache,
List<Integer>
wird nicht sehr effizient sein, unabhängig davon, wie Sie es tun. Der richtige Weg, um das zu tun, was Sie verlangen, ist einfachSELECT id FROM table
, aufSERVER SIDE
(herstellerabhängig) gesetzt und der Cursor daraufFORWARD_ONLY READ-ONLY
und iteriert darüber.Wenn Sie wirklich Millionen von IDs zur Verarbeitung ziehen, indem Sie jeweils einen Webserver aufrufen, müssen Sie auch eine gleichzeitige Verarbeitung durchführen, damit diese in einer angemessenen Zeit ausgeführt werden kann. Das Ziehen mit einem JDBC-Cursor und das gleichzeitige Platzieren einiger davon in einer ConcurrentLinkedQueue sowie das Ziehen und Verarbeiten eines kleinen Pools von Threads (# CPU / Cores + 1) ist die einzige Möglichkeit, Ihre Aufgabe auf einem Computer mit einem beliebigen " normale "RAM-Größe, vorausgesetzt, Sie haben bereits nicht genügend Speicher.
Siehe diese Antwort .
quelle
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 = 8 Byte, dann ergibt eine Liste solcher Bezeichner 10 ^ 6 ungefähr 8 MB. Wenn es sich um einen Stapelprozess handelt (jeweils eine Instanz), ist dies erträglich. Dann iterieren Sie einfach und erledigen Sie den Job.
Eine weitere Bemerkung - Sie sollten dies sowieso in Blöcken tun - insbesondere, wenn Sie Datensätze ändern, da sonst das Rollback-Segment in der Datenbank wächst.
Wenn es darum geht, die firstResult / maxRows-Strategie festzulegen, ist es SEHR SEHR langsam für Ergebnisse, die weit von der Spitze entfernt sind.
Berücksichtigen Sie auch, dass die Datenbank wahrscheinlich in einer Lese-Commit-Isolation arbeitet , um zu vermeiden, dass Phantom-Lesevorgänge Bezeichner laden und dann Entitäten einzeln (oder 10 x 10 oder was auch immer) laden.
quelle
Ich war überrascht zu sehen, dass die Verwendung gespeicherter Prozeduren in den Antworten hier nicht mehr im Vordergrund stand. In der Vergangenheit habe ich, wenn ich so etwas tun musste, eine gespeicherte Prozedur erstellt, die Daten in kleinen Blöcken verarbeitet, dann eine Weile schläft und dann fortfährt. Der Grund für das Schlafen ist, die Datenbank nicht zu überfordern, die vermutlich auch für Echtzeit-Abfragetypen verwendet wird, z. B. für die Verbindung mit einer Website. Wenn die Datenbank von niemand anderem verwendet wird, können Sie den Schlaf auslassen. Wenn Sie sicherstellen möchten, dass Sie jeden Datensatz einmal und nur einmal verarbeiten, müssen Sie eine zusätzliche Tabelle (oder ein zusätzliches Feld) erstellen, um zu speichern, welche Datensätze Sie verarbeitet haben, um bei Neustarts stabil zu sein.
Die Leistungseinsparungen sind erheblich und möglicherweise um Größenordnungen schneller als alles, was Sie in JPA / Hibernate / AppServer-Land tun können, und Ihr Datenbankserver verfügt höchstwahrscheinlich über einen eigenen serverseitigen Cursortyp für die effiziente Verarbeitung großer Ergebnismengen. Die Leistungseinsparungen ergeben sich daraus, dass die Daten nicht vom Datenbankserver an den Anwendungsserver gesendet werden müssen, wo Sie die Daten verarbeiten und dann zurücksenden.
Die Verwendung gespeicherter Prozeduren hat einige erhebliche Nachteile, die dies für Sie möglicherweise vollständig ausschließen. Wenn Sie diese Fähigkeit jedoch in Ihrer persönlichen Toolbox haben und sie in solchen Situationen einsetzen können, können Sie diese Art von Dingen ziemlich schnell ausschalten .
quelle
Um die Antwort von @Tomasz Nurkiewicz zu erweitern. Sie haben Zugriff auf die,
DataSource
die Ihnen wiederum eine Verbindung herstellen kannIn Ihrem Code haben Sie
Auf diese Weise können Sie JPA für einige bestimmte Großstapelvorgänge wie Import / Export umgehen. Bei Bedarf haben Sie jedoch weiterhin Zugriff auf den Entitätsmanager für andere JPA-Vorgänge.
quelle
Verwenden Sie
Pagination
Concept, um das Ergebnis abzurufenquelle
Ich habe mich das selbst gefragt. Es scheint wichtig zu sein:
Ich habe einen Iterator geschrieben, um das Austauschen beider Ansätze zu vereinfachen (findAll vs findEntries).
Ich empfehle Ihnen, beide zu versuchen.
Am Ende habe ich meinen Chunk-Iterator nicht verwendet (daher ist er möglicherweise nicht so getestet). Übrigens benötigen Sie Google-Sammlungen, wenn Sie es verwenden möchten.
quelle
Im Ruhezustand gibt es 4 verschiedene Möglichkeiten, um das zu erreichen, was Sie wollen. Jedes hat Design-Kompromisse, Einschränkungen und Konsequenzen. Ich schlage vor, jedes zu erkunden und zu entscheiden, welches für Ihre Situation richtig ist.
quelle
Hier ist ein einfaches, direktes JPA-Beispiel (in Kotlin), das zeigt, wie Sie über eine beliebig große Ergebnismenge paginieren können, indem Sie Teile von 100 Elementen gleichzeitig lesen, ohne einen Cursor zu verwenden (jeder Cursor verbraucht Ressourcen in der Datenbank). Es verwendet die Keyset-Paginierung.
Unter https://use-the-index-luke.com/no-offset finden Sie das Konzept der Keyset-Paginierung und unter https://www.citusdata.com/blog/2016/03/30/five-ways-to- paginieren / für einen Vergleich verschiedener Arten der Paginierung mit ihren Nachteilen.
quelle
Ein Beispiel, bei dem JPA und NativeQuery jedes Mal die Größe der Elemente mithilfe von Offsets abrufen
quelle