Dziedziczenie JPA @EntityGraph obejmuje opcjonalne powiązania podklas

12

Biorąc pod uwagę następujący model domeny, chcę załadować wszystkie Answers, w tym ich Values i ich podrzędne, i umieścić go w, AnswerDTOaby następnie przekonwertować na JSON. Mam działające rozwiązanie, ale cierpi na problem N + 1, którego chcę się pozbyć za pomocą ad-hoc @EntityGraph. Wszystkie powiązania są skonfigurowane LAZY.

wprowadź opis zdjęcia tutaj

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

Używając metody ad-hoc @EntityGraphw Repositorymetodzie, mogę upewnić się, że wartości są wstępnie pobierane, aby zapobiec N + 1 w Answer->Valuepowiązaniu. Chociaż mój wynik jest w porządku, jest jeszcze jeden problem N + 1, z powodu leniwego ładowania selectedskojarzenia MCValues.

Za pomocą tego

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

kończy się niepowodzeniem, ponieważ selectedpole jest oczywiście tylko częścią niektórych Valuepodmiotów:

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

Jak mogę powiedzieć WZP, że próbuje pobrać selectedpowiązanie tylko w przypadku, gdy jest to wartość MCValue? Potrzebuję czegoś takiego optionalAttributePaths.

Oblepiony
źródło

Odpowiedzi:

8

Możesz użyć tylko, EntityGraphjeśli atrybut asocjacji jest częścią nadklasy, a przez to także częścią wszystkich podklas. W przeciwnym razie EntityGraphzawsze zawiedzie z tym Exception, co aktualnie otrzymujesz.

Najlepszym sposobem na uniknięcie problemu wyboru N + 1 jest podzielenie zapytania na 2 zapytania:

Pierwsze zapytanie pobiera MCValueencje za pomocą a, EntityGraphaby pobrać powiązanie odwzorowane przez selectedatrybut. Po tym zapytaniu jednostki te są następnie przechowywane w pamięci podręcznej pierwszego poziomu Hibernacji / w kontekście trwałości. Hibernacja użyje ich podczas przetwarzania wyniku drugiego zapytania.

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

Drugie zapytanie następnie pobiera Answerbyt i używa również EntityGraphdo pobrania powiązanych Valuebytów. Dla każdego Valuepodmiotu, Hibernate będzie instancji konkretną podklasę i sprawdzić czy cache poziom 1 zawiera już obiekt dla tej klasy i podstawowej kombinacji klawiszy. W takim przypadku Hibernacja używa obiektu z pamięci podręcznej 1. poziomu zamiast danych zwróconych przez zapytanie.

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

Ponieważ już pobraliśmy wszystkie MCValuejednostki z powiązanymi selectedjednostkami, teraz otrzymujemy Answerjednostki z zainicjowanym valuepowiązaniem. A jeśli powiązanie zawiera MCValuebyt, jego selectedpowiązanie również zostanie zainicjowane.

Thorben Janssen
źródło
Pomyślałem o dwóch zapytaniach, pierwszym do pobrania odpowiedzi + wartość i drugim do pobrania selecteddla odpowiedzi, które mają MCValue. Nie podobało mi się, że wymagałoby to dodatkowej pętli i musiałem zarządzać mapowaniem między zestawami danych. Podoba mi się twój pomysł wykorzystania w tym celu pamięci podręcznej Hibernacji. Czy potrafisz wyjaśnić, na ile bezpieczne (pod względem spójności) jest poleganie na pamięci podręcznej w celu przechowywania wyników? Czy to działa, gdy zapytania są dokonywane w transakcji? Boję się trudnych do wykrycia i sporadycznych, leniwych błędów inicjalizacji.
Utknąłem
1
Musisz wykonać oba zapytania w ramach tej samej transakcji. Dopóki to zrobisz i nie wyczyścisz swojego kontekstu uporczywości, jest to całkowicie bezpieczne. Pamięć podręczna pierwszego poziomu zawsze będzie zawierać MCValueencje. I nie potrzebujesz dodatkowej pętli. Powinieneś pobrać wszystkie MCValueencje za pomocą 1 zapytania, które łączy się z Answertą samą klauzulą ​​WHERE, co bieżące zapytanie. Mówiłem o tym również w dzisiejszym streamie na żywo: youtu.be/70B9znTmi00?t=238 Zaczęło się o 3:58, ale zadałem kilka innych pytań pomiędzy ...
Thorben Janssen
Świetnie, dziękuję za kontynuację! Chcę również dodać, że to rozwiązanie wymaga 1 zapytania na podklasę. Utrzymanie jest więc dla nas w porządku, ale to rozwiązanie może nie być odpowiednie dla wszystkich przypadków.
Utknąłem
Muszę trochę poprawić mój ostatni komentarz: Oczywiście potrzebujesz tylko zapytania dla podklasy, która cierpi z powodu problemu. Warto również zauważyć, że w przypadku atrybutów podklas wydaje się, że nie stanowi to problemu ze względu na użycie SINGLE_TABLE_INHERITANCE.
Utknąłem
7

Nie wiem, co tam robi Spring-Data, ale aby to zrobić, zwykle musisz użyć TREAToperatora, aby uzyskać dostęp do sub-asocjacji, ale implementacja dla tego operatora jest dość błędna. Hibernacja obsługuje niejawny dostęp do właściwości podtypu, który byłby tutaj potrzebny, ale najwyraźniej Spring-Data nie obsługuje tego poprawnie. Polecam rzucić okiem na Blaze-Persistence Entity-Views , bibliotekę, która działa na bazie JPA, która pozwala mapować dowolne struktury na podstawie modelu encji. Możesz zmapować swój model DTO w sposób bezpieczny dla typu, również w strukturze dziedziczenia. Widoki encji dla Twojego przypadku użycia mogą wyglądać następująco

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

Dzięki wiosennej integracji danych zapewnianej przez Blaze-Persistence możesz zdefiniować takie repozytorium i bezpośrednio wykorzystać wynik

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

Wygeneruje zapytanie HQL, które wybiera tylko to, co zostało zamapowane, AnswerDTOco jest podobne do następującego.

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
źródło
Hmm, dziękuję za podpowiedź do twojej biblioteki, którą już znalazłem, ale nie wykorzystalibyśmy jej z 2 głównych powodów: 1) nie możemy polegać na wsparciu lib przez cały czas trwania naszego projektu (blazebit twojej firmy jest raczej mały i na początku). 2) Nie zobowiązalibyśmy się do bardziej złożonego stosu technologicznego w celu optymalizacji pojedynczego zapytania. (Wiem, że twoja biblioteka może zrobić więcej, ale wolimy wspólny stos technologii i raczej wdrożymy niestandardowe zapytanie / transformację, jeśli nie ma rozwiązania JPA).
Utknąłem
1
Blaze-Persistence jest oprogramowaniem typu open source, a Entity-Views jest mniej więcej zaimplementowany na bazie standardu JPQL / HQL. Funkcje, które implementuje są stabilne i nadal będą działać z przyszłymi wersjami Hibernacji, ponieważ działa on ponad standard. Rozumiem, że nie chcesz wprowadzać czegoś z powodu pojedynczego przypadku użycia, ale wątpię, że to jedyny przypadek użycia, do którego można użyć widoków encji. Wprowadzenie widoków encji zwykle prowadzi do znacznego zmniejszenia ilości kodu typu „podstawa”, a także do zwiększenia wydajności zapytań. Jeśli nie chcesz używać narzędzi, które ci pomogą, niech tak będzie.
Christian Beikov
Przynajmniej nie rozumiesz problemu i zapewniasz rozwiązanie. Dostajesz nagrodę, nawet jeśli odpowiedzi nie wyjaśniają, co dokładnie dzieje się w pierwotnym problemie i jak JPA może to rozwiązać. Z mojego punktu widzenia po prostu nie jest obsługiwany przez JPA i powinien stać się żądaniem funkcji. Oferuję kolejną nagrodę za bardziej rozbudowaną odpowiedź skierowaną wyłącznie na WZP.
Utknąłem
Z JPA jest to po prostu niemożliwe. Potrzebujesz operatora TREAT, który nie jest w pełni obsługiwany przez żadnego dostawcę JPA, ani nie jest obsługiwany w adnotacjach EntityGraph. Jedynym sposobem, w jaki można to wymodelować, jest funkcja rozstrzygania właściwości ukrytych podtypów Hibernacja, która wymaga użycia jawnych połączeń.
Christian Beikov
1
W twojej odpowiedzi definicja widoku powinna brzmiećinterface MCValueDTO extends ValueDTO { @Mapping("selected.id") Set<Long> getOption(); }
Stuck
0

Mój najnowszy projekt wykorzystywał GraphQL (pierwszy dla mnie) i mieliśmy duży problem z zapytaniami N + 1 i próbowałem zoptymalizować zapytania, aby łączyć się tylko dla tabel, gdy są one wymagane. Uważam, że Cosium / spring-data-jpa-entity-graph jest niezastąpiony. Rozszerza JpaRepositoryi dodaje do zapytania metody przekazywania wykresu encji. Następnie można budować dynamiczne wykresy encji w czasie wykonywania, aby dodać lewe sprzężenia tylko dla potrzebnych danych.

Nasz przepływ danych wygląda mniej więcej tak:

  1. Odbierz żądanie GraphQL
  2. Analizuj zapytanie GraphQL i konwertuj na listę węzłów grafu encji w zapytaniu
  3. Utwórz wykres encji z wykrytych węzłów i przekaż do repozytorium w celu wykonania

Aby rozwiązać problem __typenameniewłączania niepoprawnych węzłów do wykresu encji (na przykład z graphql), stworzyłem klasę narzędziową, która obsługuje generowanie wykresu encji. Klasa wywołująca przekazuje nazwę klasy, dla której generuje wykres, który następnie sprawdza poprawność każdego węzła na wykresie względem metamodelu obsługiwanego przez ORM. Jeśli węzeł nie znajduje się w modelu, usuwa go z listy węzłów wykresu. (Ta kontrola musi być rekurencyjna i sprawdzać także każde dziecko)

Zanim to znalazłem, wypróbowałem projekcje i każdą inną alternatywę zalecaną w dokumentach Spring JPA / Hibernate, ale nic nie wydawało się rozwiązać problemu elegancko lub przynajmniej z dużą ilością dodatkowego kodu

aarbor
źródło
jak rozwiązuje problem ładowania skojarzeń, które nie są znane z super typu? Ponadto, jak powiedziano na drugą odpowiedź, chcemy wiedzieć, czy istnieje czyste rozwiązanie JPA, ale myślę również, że lib cierpi z powodu tego samego problemu, że selectedpowiązanie nie jest dostępne dla wszystkich podtypów value.
Utknąłem
Jeśli interesuje Cię GraphQL, mamy również integrację widoków encji Blaze-Persistence z graphql-java: persistence.blazebit.com/documentation/1.5/entity-view/manual/…
Christian Beikov
@ChristianBeikov dzięki, ale używamy SQPR do programowego generowania naszego schematu z naszych modeli / metod
aarbor
Jeśli podoba Ci się podejście oparte na kodzie, spodoba ci się integracja z GraphQL. Obsługuje automatyczne pobieranie tylko faktycznie używanych kolumn / wyrażeń, zmniejszając złączenia itp.
Christian Beikov,
0

Edytowane po twoim komentarzu:

Przepraszam, nie zastanawiałem się nad problemem w pierwszej rundzie, problem pojawia się przy uruchomieniu danych wiosny, nie tylko podczas próby wywołania findAll ().

Możesz teraz nawigować po pełnym przykładzie, który możesz pobrać z mojego github: https://github.com/bdzzaid/stackoverflow-java/blob/master/jpa-hibernate/

Możesz łatwo odtworzyć i naprawić swój problem w tym projekcie.

W rzeczywistości dane Spring i hibernacja nie są w stanie domyślnie określić „wybranego” wykresu i należy określić sposób zbierania wybranej opcji.

Najpierw musisz zadeklarować NamedEntityGraphs klasy Answer

Jak widać, istnieją dwa NamedEntityGraph dla wartości atrybutu klasy Answer

  • Pierwszy dla wszystkich Wartość bez określonego związku do załadowania

  • Drugi dla konkretnej wartości Multichoice . Jeśli go usuniesz, odtworzysz wyjątek.

Po drugie, musisz być w kontekście transakcyjnym answerRepository.findAll (), jeśli chcesz pobierać dane typu LAZY

@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
źródło
Problemem nie jest pobierania value-association się Answerjednak coraz selectedstowarzyszenie w przypadku, gdy valuejest MCValue. Twoja odpowiedź nie zawiera żadnych informacji na ten temat.
Utknąłem
@ Utknął Dziękuję za odpowiedź, czy możesz podzielić się ze mną klasą MCValue, postaram się odtworzyć problem lokalnie.
bdzzaid
Twój przykład działa tylko dlatego, że zdefiniowałeś powiązanie OneToManyjako, FetchType.EAGERale jak stwierdzono w pytaniu: wszystkie skojarzenia są LAZY.
Zatrzymany
@Stuck Zaktualizowałem swoją odpowiedź od czasu ostatniej aktualizacji. Mam nadzieję, że moja odpowiedź pomoże Ci rozwiązać problem i pomoże ci zrozumieć sposób ładowania wykresu encji, w tym relacji opcjonalnych.
bdzzaid
W twoim „rozwiązaniu” nadal występuje pierwotny problem N + 1, o który chodzi w tym pytaniu: umieść metody insert i find w różnych transakcjach testu, a zobaczysz, że jpa wyda zapytanie DB selecteddla każdej odpowiedzi zamiast ładować je z góry.
Utknąłem