Wie speichere und lade ich eine große Graph-Struktur mit JPA und Hibernate?

8

Ich versuche, die folgende einfache Struktur (ähnlich einem gerichteten Graphen) zu erhalten und zu laden, indem ich JPA 2.1 , Hibernate 4.3.7 und Spring Data :

Graph.java

%Vor%

Node.java

%Vor%

Das Problem

In den meisten Fällen ist das Lazy-Ladeverhalten in Ordnung. Das Problem ist, dass ich in einigen Fällen in meiner Anwendung ein bestimmtes Diagramm (einschließlich aller trägen Referenzen) vollständig laden muss und auch ein vollständiges Diagramm auf eine effiziente Weise beibehalten muss, ohne Ausführen von N + 1 SQL-Abfragen . Auch wenn Speichern ein neues Diagramm, ich bekomme ein StackOverflowError , sobald der Graph zu groß wird (& gt; 1000 Knoten).

Fragen

  1. Wie kann ich ein neues Diagramm in der Datenbank mit über 10.000 Knoten speichern, da Hibernate in einem Diagramm mit 1000 Knoten mit einem StackOverflowError bereits zu ersticken scheint? Irgendwelche nützlichen Tricks?

  2. Wie kann ich ein Diagramm vollständig laden und alle verzögerten Referenzen auflösen, ohne N + 1 SQL-Abfragen durchzuführen?

Was ich bisher versucht habe

Ich habe keine Ahnung, wie ich das Problem lösen kann 1). Wie für Problem 2), habe ich versucht, die folgende HQL-Abfrage zu verwenden:

Ich versuche es gerade mit HQL mit Fetch Joins:

%Vor%

... wobei 1 sich auf einen String-Parameter bezieht, der die Graph-ID enthält. Dies scheint jedoch zu einem SQL SELECT pro Knoten zu führen, der im Graphen gespeichert ist, was zu einer schrecklichen Leistung bei Graphen mit mehreren tausend Knoten führt. Die Verwendung von Hibernates FetchProfiles ergab das gleiche Ergebnis.

Wichtig - EDIT -

EDIT 1: Es stellt sich heraus, dass Spring Data JpaRepositories ihre Operation save(T) ausführen, indem sie zuerst entityManager.merge(...) aufrufen und dann entityManager.persist(... ) aufrufen. Das StackOverflowError funktioniert nicht in einem "rohen" entityManager.persist(...) , aber es tritt in entityManager.merge(...) auf. Es löst das Problem jedoch immer noch nicht, warum passiert dies bei einer Zusammenführung?

EDIT 2: Ich denke, das ist wirklich ein Fehler in Hibernate. Ich habe einen Fehlerbericht mit einem vollständigen, unabhängigen JUnit-Testprojekt eingereicht. Falls jemand interessiert ist, können Sie es hier finden: Hibernate JIRA

Zusatzmaterial

Hier ist die Klasse PersistableObject , die eine UUID für ihre @ID verwendet, und eine Eclipse-generierte Methode hashCode() und equals(...) basierend auf dieser ID.

PersistableObject.java

%Vor%

Wenn Sie es selbst ausprobieren möchten, hier ist eine Fabrik, die eine zufällige Grafik erzeugt:

GraphFactory.java

%Vor%

Der Stack-Trace

Der Stack-Trace von StackOverflowError enthält wiederholt die folgende Sequenz (direkt nacheinander):

%Vor%     
Alan47 12.01.2015, 12:48
quelle

1 Antwort

5

Während der letzten 24 Stunden habe ich viel über dieses Thema geforscht und werde versuchen, hier eine vorläufige Antwort zu geben. Bitte korrigieren Sie mich, wenn ich etwas falsch mache.

Problem: Hibernate StackOverflowException auf entityManager.merge (...)

Dies scheint ein generelles Problem mit ORM zu sein. Von Natur aus ist der "Merge" -Algorithmus rekursiv. Wenn in Ihrem Modell ein Pfad (von Entität zu Entität) vorhanden ist, der zu viele Entitäten enthält, ohne jemals auf eine bekannte Entität zu verweisen, ist die Rekursionstiefe des Algorithmus größer als die Stackgröße Ihrer JVM.

Lösung 1: Erhöhen Sie die Stapelgröße Ihrer JVM

Wenn Sie wissen, dass Ihr Modell nur geringfügig zu groß für die Stapelgröße Ihrer JVM ist, können Sie diesen Wert erhöhen, indem Sie den Startparameter -Xss (und einen geeigneten Wert) verwenden, um ihn zu erhöhen . Beachten Sie jedoch, dass dieser Wert statisch ist. Wenn Sie also ein größeres Modell als zuvor laden, müssen Sie es erneut erhöhen.

Lösung 2: Aufbrechen der Entitätsketten

Dies ist definitiv keine Lösung im Sinne von Object-Relational Mapping, aber nach meinem derzeitigen Wissen ist es die einzige Lösung, die effektiv mit wachsender Modellgröße skaliert. Die Idee ist, dass Sie eine normale Java-Referenz in Ihren @Entity -Klassen durch einen primitiven Wert ersetzen, der stattdessen den Wert @Id der Zieleinheit enthält. Wenn Ihr Ziel @Entity einen ID-Wert vom Typ long verwendet, müssten Sie einen long -Wert speichern. Es ist dann Aufgabe der Anwendungsebene, den Verweis nach Bedarf aufzulösen (indem eine findById(...) -Abfrage in der Datenbank ausgeführt wird).

Wenn wir das Diagrammszenario aus dem Fragenpost übernommen haben, müssten wir die Klasse Node auf diese ändern:

%Vor%

Problem: N + 1 SQL wählt

aus

Ich wurde hier von Spring und Hibernate getäuscht. Mein Komponententest verwendete JpaRepository und repository.save(graph) gefolgt von repository.fullyLoadById(graphId) (mit einer @Query -Anmerkung unter Verwendung der HQL-Fetch-Join-Abfrage aus dem Fragenpost) und maß die Zeit für jede Operation. Die SQL-Select-Abfragen, die in meinem Konsolenprotokoll auftauchten, stammten von nicht von der fullyLoadById -Abfrage, aber von repository.save(graph) . Was Spring-Repositorys hier tun, ist, zuerst entityManager.merge(...) für das zu speichernde Objekt aufzurufen. Zusammenführen ruft wiederum den aktuellen Status der Entität aus der Datenbank ab. Dieses Abrufen führt zu der großen Anzahl von SQL-Select-Anweisungen, die ich erlebt habe. Meine Ladeabfrage wurde tatsächlich wie beabsichtigt in einer einzigen SQL-Abfrage ausgeführt.

Lösung:

Wenn Sie ein ziemlich großes Objektdiagramm haben und wissen, dass es definitiv neu ist, nicht in der Datenbank enthalten ist und keine Entität referenziert, die in der Datenbank gespeichert ist, können Sie den Schritt merge(...) überspringen und direkt aufrufen entityManager.persist(...) für bessere Leistung. Frühlingsrepositorys verwenden immer merge(...) aus Sicherheitsgründen. persist(...) versucht eine SQL INSERT -Anweisung, die fehlschlägt , wenn bereits eine Zeile mit der angegebenen ID in der Datenbank vorhanden ist.

Beachten Sie auch, dass Hibernate immer alle Abfragen einzeln protokolliert, wenn Sie hibernate.show_sql = true verwenden. JDBC-Batching findet statt, nachdem die Abfragen generiert wurden. Wenn Sie also viele Abfragen in Ihrem Protokoll sehen, bedeutet das nicht unbedingt, dass Sie so viele DB-Roundtrips hatten.

    
Alan47 13.01.2015 08:47
quelle