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% 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).
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?
Wie kann ich ein Diagramm vollständig laden und alle verzögerten Referenzen auflösen, ohne N + 1 SQL-Abfragen durchzuführen?
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.
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
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):
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.
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.
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.
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:
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.
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.
Tags und Links jpa spring-data-jpa hibernate graph