JPA - eager i n+1

0

Jako. że stosuję raczej CQRS i mam osobne modele do odczytu i zapisu to nie miałem od dawna wielkich bolączek z JPA, aż do czasu ..

Czy coś się zmieniło jeśli chodzi o zastosowanie ładowanie EAGER dla @OneToMany? Bo nawet z ładowaniem "zachłannym" występuje problem N+1, a dałbym sobie rękę uciąć, że wiele osób mówiło, że jak damy EAGER to nie ma n+1. A sprawdziłem i jest.
To w sumie po co jest to całe EAGER? Jest jakieś zastosowanie na to?

Załóżmy, że mamy strukturę Topic -> Posts -> Comments i chcemy zwrócić to jako dtosy na front (tak, takie całe drzewko).

Ja to po prostu robię tak, że mam 3 selecty do bazy i potem to w kodzie javy grupuję i jest ok.

A załóżmy, że ktoś ma w topic @OneToMany na posty i w poście @OneToMany na komentarze. I jak to teraz zmapować najlepiej na dtosy? No można batchami.
Jest jeszcze rozwiązanie, że w repo nad metodą Set<Topic> findAll() dać customowe @Query z fetch joinem. Jak to się sprawdza? Jest ok takie rozwiązanie?

0

EAGER mówy o tym, że dane mają zostać załadowane, a nie w jaki sposób. Aby zoptymalizować zapytania do bazy danych jest parę sposobów, np:

  • zastosować EntityGraph z pakietu javax.persistence
  • w przypadku hibernate użyć FetchMode (z pakietu hibernate, jest to coś innego niż FetchMode z pakietu javax.persistence)
  • w zależności czy to aplikacja Spring czy JavaEE, pewnie da się poustawiać jakieś konfiguracje w application.yml czy persistence.xml
0

Ok dobra .. to teraz tak.
Jaka jest różnica między ładowaniem EAGER a FetchMode = JOIN ? Bo jak sprawdzam to obie po prostu ładują gorliwie, ale nadal jest n+1 problem mimo tego co jest napisane tu: https://www.baeldung.com/hibernate-fetchmode

Natomiast fajnie działa FetchMode = SUBSELECT. Bo i mam LAZY i nie mam n+1. Co prawda nie ładuję wszystkiego 1 zapytaniem tylko dwoma, ale nie mam kwestii z iloczynem kartezjanńskim.

Dlaczego w sumie wszędzie się nie używa właśnie LAZY + FetchMode = SUBSELECT? TO chyba najlepsze rozwiązanie.

0

A to nie zależy jeszcze od dwóch poziomów cache'y? https://vladmihalcea.com/n-plus-1-query-problem/

0

@Pinek:
Rozwiniesz?
Ja nie mam włączonego akurat second lvl cache tutaj gdzie to testuję sobie.

0

Postaram się wyjaśnić w wielkim skrócie, FetchType to jest rodzaj żądania, czy dane mają być załadowane wcześniej czy leniwie, jeszcze nie mówimy nic o zapytaniach SQL pod spodem. Jak jest EAGER to po JPA zapewnia, że po wyjściu z transakcji obiekt będzie załadowany, jak LAZY to taka obietnica nie musi być spełniona. I tyle. A teraz FetchMode mówi w jaki sposób pod spodem zapytania mają zostać wykonane jeśli dojdzie do ładowania elementów. Czyli FetchType mówi o efekcie końcowym, a FetchMode w jaki sposób dojść do żądanego efektu. Teraz porozmawiajmy o encjach zarządzanych. Jeśli w transakcji jakiś obiekt został już załadowany to nie jest on ładowany drugi raz, ponieważ hibernate pobiera go sobie z cache. Czyli jakbyś najpierw w jednej transakcji pobrał listę postów po id topicu, a następnie pobrałbyś sam topic to w zapytaniach SQL zauważyłbyś, że podczas pobierania topicu posty nie są pobierane, ponieważ hibernate wie, że ma te obiekty z cache'a. Można powiedzieć, że cache hibernate to takie hashmapy na każdy typ encji, gdzie kluczem są ID encji, a wartością encje. Jak encja znajduje się w hashmapie to znaczy, że jest zarządzana. Jak już coś zostało załadowane w transakcji to nie ładuje tego drugi raz (chyba że obiekt się zmienił i znowu zostało wywołane jakieś zapytanie wybierające dane, ale to inny temat). Hibernate nie stawia na szybkość, tylko na nieprzejmowanie się przez programistę synchronizacją obiektów pomiędzy aplikacją a bazą danych. A teraz trochę taka moja rada, wrzucanie do encji informacji, czy jej pola mają być ładowane LAZY czy EAGER zmieniając domyślne zachowania spowoduje, że potem jakbyś chciał wyświetlić listę topiców to pod spodem dla każdego topica załadujesz całą listę postów, a potem pewnie i całą listę komentarzy. I to dla każdego użycia encji w aplikacji. Proponuję użyć EntityGraph, tam jako parametr w zapytaniu go podajesz, i na każdy przypadek możesz mieć zapytania wyciągające obiekt z załadowanymi polami albo nie. A druga sprawa to polecałbym robienie jak najbardziej płaskiego dto jak się tylko da, bez zagnieżdżania pod spodem obiektów, niech frontend dociąga sobie dwoma wołaniami CQRS, najpierw listę topiców, potem po wejściu na topic po topic id niech dociągnie listę postów.

0

@ivo:

Ale ja rozumiem jak to działa, bo przetestowałem sobie.
Zastanawiam się tylko nad poniższymi kwestiami:

  1. Kiedy i po co używa się EAGER?
  2. Dlaczego nie używa się głównie LAZY + FetchMode = SUBSELECT?
  3. Po co używać FetchMode.JOIN zamiast EAGER?

Chodzi mi praktyczne odpowiedzi ;p

0
Bambo napisał(a):

Zastanawiam się tylko nad poniższymi kwestiami:

  1. Kiedy i po co używa się EAGER?

Np. kiedy chcesz zaciągnąć listę wartości i na niej operować, ale chcesz też jak najszybciej zwolnić transakcję bazodanową.

  1. Dlaczego nie używa się głównie LAZY + FetchMode = SUBSELECT?
  • bo nie zawsze trzeba
  • bo nie zawsze się o tym wie
  • bo nie zawsze warto - różne silniki bazodanowe mogą różnie subselecta ogarniać, w dodatku możesz mieć jeszcze konfigurację cache'y
  1. Po co używać FetchMode.JOIN zamiast EAGER?

Nie należy używać EAGER. EAGER jest depreciated bo używa jakichś tam starszych wartości więc z czasem wyleci.

0

@Bambo:

  1. EAGER jest domyślnie dla pól, które nie są kolekcjami, pozostałe powody to założenia projektu, czy występują jakieś encje, które powinny zawsze mieć załadowaną kolekcję, nie ma na to ogólnej zasady.
  2. FetchMode może mają wpływ na to, czy wartość trafi do cache czy nie, wydaje mi się że FetchMode.SELECT jest dla hibernate łatwiejszy do sprawdzenia, czy w danej transakcji encja była już załadowana, bo każda encja jest oddzielnym zapytaniem (czyli jest jakiś logiczny powód dla którego występuje n+1).
  3. FetchType jest w JPA, FetchMode jest w Hibernate, czasami pisze się aplikacje w JPA bez używania rozszerzeń Hibernate, ponieważ nie tylko Hibernate jest implementacją standardu JPA, istnieje także EclipseLink.
0

Nie bardzo rozumiem jak EAGER ma wpływać na szybkość wyjścia z transakcji i założenia projektowe?

Przecież chyba zawsze załadować lepiej to jako LAZY no nie? Nawet jeśli wiem, że będę operował na całej kolekcji bo np musze coś z niej zsumować to jak mam LAZY to mi po prostu pobierze kolekcje jak się do niej odwołam.

Nawet jeśli każda metody w encji potrzebuje kolekcji w środku to nadal nie widzę różnicy w praktyce między EAGER a LAZY. No chyba, że tu chodzi o jakieś optymalizację ?

0

Co do szybkości wyjścia z transakcji to też nie rozumiem. A odnośnie założeń projektowych to chodziło mi o to, że bywają przypadki, że na jakimś polu ustawia się jawnie EAGER, np. w systemie spamującym z użytkownikiem wyciąga się wszystkie jego adresy emailowe, bo 99% operacji na użytkowniku w tym systemie to i tak odwoływanie się do jego adresow email.
W @OneToOne oraz @ManyToOne domyślnie jest EAGER, w @OneToMany oraz @ManyToMany jest domyślnie LAZY, i w większości przypadków najlepiej te domyślne wartości zostawiać takie jakie są, jak w transakcji trzeba dobrać się do kolekcji np. do zsumowania, to się dociągnie kolekcja, jeśli to nie jest aplikacja, gdzie liczą się ułamki sekund to uważam, że nie trzeba tego zmieniać.
Różnice między EAGER a LAZY widać jak wychodzi się z transakcji, jak obiekty przestają być zarządzane, wtedy jak kolekcja jest niezaładowana to przy odwołaniu do jej elementów leci LazyInitializationException, a to nie jest powód, aby teraz wszystkie powiązania oznaczać jako EAGER, bo nie bez powodu domyślne wartości są domyślnymi.
Z kolei w swojej karierze widziałem aplikację, gdzie ktoś wpadł na pomysł, aby wszystkie relacje w encji ustawiać na LAZY, bo będzie szybciej, bo przecież joiny w zapytaniach tyle kosztują... 5 sekund krótszy czas w zapytaniach do bazy danych na dobę "zaoszczędził" godziny pisania kodu dociągającego zależne encje, tak aby po wyjściu z transakcji nie leciał LazyInitializationException na każdym polu...

2

Skoro stosujesz Cqrs to czemu nie skorzystasz z JdbcTemplate czy jakiegoś Jooqa?

0

@ProgScibi:
A nie wiesz, że czasami ktoś już z góry przychodzi z JPA i wspólnymi klasami dla komand i querysów? I jest pro architekt, który nie chce odpowiednio skrojonych encji pod przypadki (konteksty) biznesowe i nie wie, że dla odczytów można projektować zupełnie inne read modele? I że jak masz formatkę z userem w apce to musi przecież być User encja jpa? I nie wie, że jest coś takiego jak n+1 problem?

I nie wiesz, że czasami nie da się przepchnąć po prostu inaczej? Tam gdzie mogę to oczywiście, że używam JOOQ, ale to są normalne projekty i normalni ludzie xd

Ale biorę sobie poboczne projekty z szambem bo płacą fajnie XD
Przecież nie stosowałbym czegoś takiego z własnej woli o_O.

EDIT:
Ogarnąłem już sobie, EAGER doładowuje joinem dane jak pod spodem EntityManager używa metody find(). Podobnie działa @Fetch(FetchMode.JOIN).

2
Bambo napisał(a):

Nie bardzo rozumiem jak EAGER ma wpływać na szybkość wyjścia z transakcji i założenia projektowe?

Sorry, pomyliłem z FetchType.EAGER

Przecież chyba zawsze załadować lepiej to jako LAZY no nie? Nawet jeśli wiem, że będę operował na całej kolekcji bo np musze coś z niej zsumować to jak mam LAZY to mi po prostu pobierze kolekcje jak się do niej odwołam.

Nawet jeśli każda metody w encji potrzebuje kolekcji w środku to nadal nie widzę różnicy w praktyce między EAGER a LAZY. No chyba, że tu chodzi o jakieś optymalizację ?

Oczywiście, że chodzi o optymalizację. Np. taki kod:

class MyService {
    private final MySecondService service;

    public Double compute() {
        MyEntity entity = service.subEntity();
        Predicate<MyJoinedEntity> myPredicate = createPredicate(entity.getName());

        OptionalDouble sum = entity getJoinedEntities().stream()
                .filter(myPredicate)
                .mapToDouble(MyEntity::value)
                .sum();
        
        return sum.orElse(-1);
    }

    // Time consuming call
    private Predicate<MyJoinedEntity> createPredicate(String name) {
        // ...
    }
}


class MySecondService {

    @Transactional
    public MyEntity subEntity() {
        // ... fetch data
    }
}

Oba są ogarniane przez Springa, więc jeśli jest @Transactional to transakcja jest zakładana na starcie i commitowana na końcu. Załóż że createPredicate(name) trwa bardzo długo, np. minutę. MyEntity ma pod spodem mapowanie do 1:N do JoinedEntity.

I teraz w linijce z entity.getJoinedEnitites() jeśli będzie FetchType.LAZY to w tej linijce poleci ci NPE - transakcja zakończy się przy wyjściu z MySecondService.subEntity(), a entity będzie w stanie detached.

Żeby to rozwiązać możesz:

  1. Dorzucić transakcję na compute()
  2. Zamienić FetchType na EAGER.
  3. W subEntity() zrobić coś z kolekcją pod spodem, np. zawołać entity.getJoinedEntities().size()

Opcja pierwsza blokuje transakcję - a więc jeśli nie zmieniłem ConnectionReleaseMode i połączenie w ConnectionPool dostępnym dla aplikacji - na czas wykonania createPredicate(name).
Opcja druga sprawę rozwiązuje.
Opcja trzecia też, tylko, że jest wolniejsza niż opcja druga bo zamiast od razu zaciągnąć wszystko konwersacja będzie miała dwa zapytania. Poza tym wygląda słabo - bo niby masz FetchType.LAZY, natomiast MySecondService będzie ładował to i tak od razu.

1 użytkowników online, w tym zalogowanych: 0, gości: 1