Die JPA-Vererbung @EntityGraph enthält optionale Zuordnungen von Unterklassen

12

In Anbetracht des folgenden Domänenmodells möchte ich alle Answers einschließlich ihrer Values und ihrer jeweiligen Unterkinder laden und in eine AnswerDTOablegen, um sie dann in JSON zu konvertieren. Ich habe eine funktionierende Lösung, aber sie leidet unter dem N + 1-Problem, das ich mithilfe eines Ad-hoc-Problems beseitigen möchte @EntityGraph. Alle Zuordnungen sind konfiguriert LAZY.

Geben Sie hier die Bildbeschreibung ein

@Query("SELECT a FROM Answer a")
@EntityGraph(attributePaths = {"value"})
public List<Answer> findAll();

Mit einem Ad-hoc- Verfahren @EntityGraphfür die RepositoryMethode kann ich sicherstellen, dass die Werte vorab abgerufen werden, um N + 1 für die Answer->ValueZuordnung zu verhindern . Während mein Ergebnis in Ordnung ist, gibt es ein weiteres N + 1-Problem, da die selectedAssoziation der MCValues verzögert geladen wird .

Verwenden Sie dies

@EntityGraph(attributePaths = {"value.selected"})

schlägt fehl, weil das selectedFeld natürlich nur ein Teil einiger ValueEntitäten ist:

Unable to locate Attribute  with the the given name [selected] on this ManagedType [x.model.Value];

Wie kann ich JPA mitteilen, dass nur dann versucht wird, die selectedZuordnung abzurufen, wenn der Wert a ist MCValue? Ich brauche so etwas optionalAttributePaths.

Stecken
quelle

Antworten:

8

Sie können ein nur verwenden, EntityGraphwenn das Zuordnungsattribut Teil der Oberklasse und damit auch Teil aller Unterklassen ist. Andernfalls schlägt das EntityGraphimmer mit dem fehl Exception, was Sie aktuell erhalten.

Der beste Weg, um Ihr N + 1-Auswahlproblem zu vermeiden, besteht darin, Ihre Abfrage in zwei Abfragen aufzuteilen:

Die erste Abfrage ruft die MCValueEntitäten mit a ab EntityGraph, um die durch das selectedAttribut zugeordnete Zuordnung abzurufen . Nach dieser Abfrage werden diese Entitäten im Cache der ersten Ebene von Hibernate / im Persistenzkontext gespeichert. Der Ruhezustand verwendet sie, wenn das Ergebnis der zweiten Abfrage verarbeitet wird.

@Query("SELECT m FROM MCValue m") // add WHERE clause as needed ...
@EntityGraph(attributePaths = {"selected"})
public List<MCValue> findAll();

Die zweite Abfrage ruft dann die AnswerEntität ab und verwendet eine EntityGraph, um auch die zugeordneten ValueEntitäten abzurufen . Für jede ValueEntität instanziiert Hibernate die spezifische Unterklasse und prüft, ob der Cache der ersten Ebene bereits ein Objekt für diese Kombination aus Klasse und Primärschlüssel enthält. In diesem Fall verwendet Hibernate das Objekt aus dem Cache der ersten Ebene anstelle der von der Abfrage zurückgegebenen Daten.

@Query("SELECT a FROM Answer a")
@EntityGraph(attributePaths = {"value"})
public List<Answer> findAll();

Da wir bereits alle MCValueEntitäten mit den zugeordneten selectedEntitäten abgerufen haben, erhalten wir jetzt AnswerEntitäten mit einer initialisierten valueZuordnung. Wenn die Zuordnung eine MCValueEntität enthält , selectedwird auch die Zuordnung initialisiert.

Thorben Janssen
quelle
Ich dachte darüber nach, zwei Abfragen zu haben, die erste zum Abrufen von Antworten + Wert und die zweite zum Abrufen selectedvon Antworten mit a MCValue. Ich mochte es nicht, dass dies eine zusätzliche Schleife erfordern würde und ich die Zuordnung zwischen den Datensätzen verwalten müsste. Ich mag Ihre Idee, den Hibernate-Cache dafür auszunutzen. Können Sie erläutern, wie sicher (in Bezug auf die Konsistenz) es ist, sich auf den Cache zu verlassen, um die Ergebnisse zu enthalten? Funktioniert dies, wenn die Abfragen in einer Transaktion durchgeführt werden? Ich habe Angst vor schwer zu erkennenden und sporadisch faulen Initialisierungsfehlern.
Stuck
1
Sie müssen beide Abfragen innerhalb derselben Transaktion ausführen. Solange Sie dies tun und Ihren Persistenzkontext nicht löschen, ist dies absolut sicher. Ihr Cache der ersten Ebene enthält immer die MCValueEntitäten. Und Sie brauchen keine zusätzliche Schleife. Sie sollten alle MCValueEntitäten mit einer Abfrage abrufen, die Answermit der WHERE-Klausel verknüpft ist und dieselbe WHERE-Klausel wie Ihre aktuelle Abfrage verwendet. Ich habe auch im heutigen Live-Stream darüber gesprochen: youtu.be/70B9znTmi00?t=238 Es begann um 3:58 Uhr, aber ich habe zwischendurch ein paar andere Fragen gestellt ...
Thorben Janssen
Super, danke für das Follow-up! Außerdem möchte ich hinzufügen, dass diese Lösung 1 Abfrage pro Unterklasse erfordert. Die Wartbarkeit ist für uns in Ordnung, aber diese Lösung ist möglicherweise nicht für alle Fälle geeignet.
stuck
Ich muss meinen letzten Kommentar ein wenig korrigieren: Natürlich benötigen Sie nur eine Abfrage pro Unterklasse, die unter dem Problem leidet. Es ist auch erwähnenswert, dass dies für Attribute der Unterklassen aufgrund der Verwendung kein Problem zu sein scheint SINGLE_TABLE_INHERITANCE.
Stuck
7

Ich weiß nicht, was Spring-Data dort tut, aber um dies zu tun, müssen Sie normalerweise den TREATOperator verwenden, um auf die Unterzuordnung zugreifen zu können, aber die Implementierung für diesen Operator ist ziemlich fehlerhaft. Hibernate unterstützt den impliziten Zugriff auf Subtyp-Eigenschaften, den Sie hier benötigen würden, aber anscheinend kann Spring-Data dies nicht richtig handhaben. Ich kann Ihnen empfehlen, sich Blaze-Persistence Entity-Views anzusehen , eine Bibliothek, die auf JPA aufbaut und es Ihnen ermöglicht, beliebige Strukturen Ihrem Entitätsmodell zuzuordnen. Sie können Ihr DTO-Modell typsicher zuordnen, auch die Vererbungsstruktur. Entitätsansichten für Ihren Anwendungsfall könnten folgendermaßen aussehen

@EntityView(Answer.class)
interface AnswerDTO {
  @IdMapping
  Long getId();
  ValueDTO getValue();
}
@EntityView(Value.class)
@EntityViewInheritance
interface ValueDTO {
  @IdMapping
  Long getId();
}
@EntityView(TextValue.class)
interface TextValueDTO extends ValueDTO {
  String getText();
}
@EntityView(RatingValue.class)
interface RatingValueDTO extends ValueDTO {
  int getRating();
}
@EntityView(MCValue.class)
interface TextValueDTO extends ValueDTO {
  @Mapping("selected.id")
  Set<Long> getOption();
}

Mit der von Blaze-Persistence bereitgestellten Spring-Datenintegration können Sie ein solches Repository definieren und das Ergebnis direkt verwenden

@Transactional(readOnly = true)
interface AnswerRepository extends Repository<Answer, Long> {
  List<AnswerDTO> findAll();
}

Es wird eine HQL-Abfrage generiert, die genau das auswählt, was Sie in AnswerDTOder folgenden Abbildung zugeordnet haben.

SELECT
  a.id, 
  v.id,
  TYPE(v), 
  CASE WHEN TYPE(v) = TextValue THEN v.text END,
  CASE WHEN TYPE(v) = RatingValue THEN v.rating END,
  CASE WHEN TYPE(v) = MCValue THEN s.id END
FROM Answer a
LEFT JOIN a.value v
LEFT JOIN v.selected s
Christian Beikov
quelle
Hmm, danke für den Hinweis auf Ihre Bibliothek, den ich bereits gefunden habe, aber wir würden ihn aus zwei Hauptgründen nicht verwenden: 1) Wir können uns nicht darauf verlassen, dass die Bibliothek während der gesamten Laufzeit unseres Projekts unterstützt wird (Ihr Unternehmens-Blazebit ist eher klein und in seinen Anfängen). 2) Wir würden uns nicht auf einen komplexeren Tech-Stack festlegen, um eine einzelne Abfrage zu optimieren. (Ich weiß, dass Ihre Bibliothek mehr kann, aber wir bevorzugen einen gemeinsamen Tech-Stack und würden lieber eine benutzerdefinierte Abfrage / Transformation implementieren, wenn es keine JPA-Lösung gibt.)
stuck
1
Blaze-Persistence ist Open Source und Entity-Views wird mehr oder weniger zusätzlich zu JPQL / HQL implementiert, was Standard ist. Die implementierten Funktionen sind stabil und funktionieren auch mit zukünftigen Versionen von Hibernate, da sie über dem Standard arbeiten. Ich verstehe, dass Sie aufgrund eines einzelnen Anwendungsfalls nichts einführen möchten, aber ich bezweifle, dass dies der einzige Anwendungsfall ist, für den Sie Entity Views verwenden können. Das Einführen von Entitätsansichten führt normalerweise zu einer erheblichen Reduzierung des Boilerplate-Codes und erhöht auch die Abfrageleistung. Wenn Sie keine Tools verwenden möchten, die Ihnen helfen, sollten Sie es auch tun.
Christian Beikov
Zumindest haben Sie das Problem nicht verstanden und bieten eine Lösung. Sie erhalten also das Kopfgeld, obwohl die Antworten nicht erklären, was genau im ursprünglichen Problem vor sich geht und wie JPA es lösen könnte. Meiner Meinung nach wird es von JPA einfach nicht unterstützt und sollte zu einer Funktionsanforderung werden. Ich werde eine weitere Prämie für eine ausführlichere Antwort anbieten, die nur auf JPA abzielt.
stuck
Mit JPA ist das einfach nicht möglich. Sie benötigen den TREAT-Operator, der weder von einem JPA-Anbieter noch von den EntityGraph-Annotationen vollständig unterstützt wird. Die einzige Möglichkeit, dies zu modellieren, besteht in der Funktion zum Auflösen impliziter Subtyp-Eigenschaften für den Ruhezustand, für die explizite Verknüpfungen verwendet werden müssen.
Christian Beikov
1
In Ihrer Antwort sollte die Ansichtsdefinition lauteninterface MCValueDTO extends ValueDTO { @Mapping("selected.id") Set<Long> getOption(); }
Stuck
0

Mein letztes Projekt verwendete GraphQL (eine Premiere für mich) und wir hatten ein großes Problem mit N + 1-Abfragen und versuchten, die Abfragen so zu optimieren, dass sie nur dann für Tabellen verknüpft werden, wenn sie benötigt werden. Ich habe festgestellt, dass Cosium / spring-data-jpa-entity-graph unersetzlich ist. Es erweitert JpaRepositoryund fügt Methoden zum Übergeben eines Entitätsdiagramms an die Abfrage hinzu. Anschließend können Sie zur Laufzeit dynamische Entitätsdiagramme erstellen, um nur für die benötigten Daten Linksverknüpfungen hinzuzufügen.

Unser Datenfluss sieht ungefähr so ​​aus:

  1. GraphQL-Anfrage empfangen
  2. Analysieren Sie die GraphQL-Anforderung und konvertieren Sie sie in eine Liste der Entitätsdiagrammknoten in der Abfrage
  3. Erstellen Sie ein Entitätsdiagramm aus den erkannten Knoten und übergeben Sie es zur Ausführung an das Repository

Um das Problem zu lösen, dass ungültige Knoten nicht in das Entitätsdiagramm aufgenommen werden (z. B. __typenameaus graphql), habe ich eine Dienstprogrammklasse erstellt, die die Generierung des Entitätsdiagramms übernimmt. Die aufrufende Klasse übergibt den Klassennamen, für den sie das Diagramm generiert, und validiert dann jeden Knoten im Diagramm anhand des vom ORM verwalteten Metamodells. Wenn sich der Knoten nicht im Modell befindet, wird er aus der Liste der Diagrammknoten entfernt. (Diese Prüfung muss rekursiv sein und auch jedes Kind prüfen.)

Bevor ich dies fand, hatte ich Projektionen und jede andere in den Spring JPA / Hibernate-Dokumenten empfohlene Alternative ausprobiert, aber nichts schien das Problem elegant oder zumindest mit einer Menge zusätzlichen Codes zu lösen

aarbor
quelle
Wie löst es das Problem des Ladens von Assoziationen, die vom Supertyp nicht bekannt sind? Wie bereits in der anderen Antwort erwähnt, möchten wir wissen, ob es eine reine JPA-Lösung gibt, aber ich denke auch, dass die Bibliothek unter dem gleichen Problem leidet, dass die selectedZuordnung nicht für alle Untertypen von verfügbar ist value.
stuck
Wenn Sie an GraphQL interessiert sind, haben wir auch eine Integration von Blaze-Persistence Entity Views mit graphql-java: persistence.blazebit.com/documentation/1.5/entity-view/manual/…
Christian Beikov
@ChristianBeikov danke, aber wir verwenden SQPR, um unser Schema programmgesteuert aus unseren Modellen / Methoden zu generieren
aarbor
Wenn Sie den Code-First-Ansatz mögen, werden Sie die GraphQL-Integration lieben. Es werden nur die tatsächlich verwendeten Spalten / Ausdrücke abgerufen, wodurch Verknüpfungen usw. automatisch reduziert werden.
Christian Beikov
0

Nach Ihrem Kommentar bearbeitet:

Ich entschuldige mich, ich habe Ihr Problem in der ersten Runde nicht verstanden. Ihr Problem tritt beim Start von Spring-Daten auf, nicht nur, wenn Sie versuchen, findAll () aufzurufen.

So können Sie jetzt navigieren. Das vollständige Beispiel kann von meinem Github abgerufen werden: https://github.com/bdzzaid/stackoverflow-java/blob/master/jpa-hibernate/

Sie können Ihr Problem in diesem Projekt einfach reproduzieren und beheben.

Tatsächlich können Spring-Daten und der Ruhezustand das "ausgewählte" Diagramm standardmäßig nicht bestimmen, und Sie müssen angeben, wie die ausgewählte Option erfasst werden soll.

Sie müssen also zuerst die NamedEntityGraphs der Klasse Answer deklarieren

Wie Sie sehen können, gibt es zwei NamedEntityGraph für das Attribut - Wert der Klasse Antwort

  • Der erste für alle Wert ohne spezifische Beziehung zur Last

  • Die zweite für den spezifischen Multichoice- Wert. Wenn Sie diese entfernen, reproduzieren Sie die Ausnahme.

Zweitens müssen Sie sich in einem Transaktionskontext antworten. AnswerRepository.findAll (), wenn Sie Daten vom Typ LAZY abrufen möchten

@Entity
@Table(name = "answer")
@NamedEntityGraphs({
    @NamedEntityGraph(
            name = "graph.Answer", 
            attributeNodes = @NamedAttributeNode(value = "value")
    ),
    @NamedEntityGraph(
            name = "graph.AnswerMultichoice",
            attributeNodes = @NamedAttributeNode(value = "value"),
            subgraphs = {
                    @NamedSubgraph(
                            name = "graph.AnswerMultichoice.selected",
                            attributeNodes = {
                                    @NamedAttributeNode("selected")
                            }
                    )
            }
    )
}
)
public class Answer
{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(updatable = false, nullable = false)
    private int id;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "value_id", referencedColumnName = "id")
    private Value value;
// ..
}
bdzzaid
quelle
Das Problem besteht nicht darin , die Assoziation valuevon abzurufen, Answersondern die selectedAssoziation zu erhalten, falls valuees sich um eine handelt MCValue. Ihre Antwort enthält keine diesbezüglichen Informationen.
stuck
@Stuck Vielen Dank für Ihre Antwort. Können Sie mir bitte die Klasse MCValue mitteilen? Ich werde versuchen, Ihr Problem lokal zu reproduzieren.
Bdzzaid
Ihr Beispiel funktioniert nur, weil Sie die Zuordnung OneToManyso definiert haben FetchType.EAGER, wie in der Frage angegeben: Alle Zuordnungen sind LAZY.
stuck
@Stuck Ich habe meine Antwort seit Ihrem letzten Update aktualisiert. Ich hoffe, dass meine Antwort Ihnen bei der Lösung Ihres Problems hilft und Ihnen hilft, die Art und Weise zu verstehen, wie ein Entitätsdiagramm einschließlich optionaler Beziehungen geladen wird.
Bdzzaid
Ihre "Lösung" leidet immer noch unter dem ursprünglichen N + 1-Problem, um das es bei dieser Frage geht: Fügen Sie Einfüge- und Suchmethoden in verschiedene Transaktionen Ihres Tests ein, und Sie sehen, dass jpa selectedfür jede Antwort eine DB-Abfrage ausgibt , anstatt sie im Voraus zu laden.
Stuck