Problem z n+1 zapytań przy paginacji i fetchowaniu relacji w JPA

Problem z n+1 zapytań przy paginacji i fetchowaniu relacji w JPA
XW
  • Rejestracja:ponad rok
  • Ostatnio:7 miesięcy
  • Postów:19
0

Cześć,

Mam problem, z którym zmagam się od jakiegoś czasu i liczę na Waszą pomoc.

Posiadam abstrakcyjną klasę Person, która jest encją, po której dziedziczą trzy inne klasy: Teacher, Principal i StaffMember – wszystkie również są encjami. Wszystkie te trzy typy osób są przechowywane w jednej tabeli Person, a zarządzam nimi za pomocą wspólnego repozytorium PersonRepository.

Klasa Person zawiera pola wspólne dla wszystkich typów osób, takie jak imię, nazwisko, PESEL, email, itd. Natomiast każda z dziedziczących klas ma swoje specyficzne pola – np. yearsAsPrincipal dla dyrektora, czy assignedClasses (lista klas) dla nauczyciela. Dodatkowo, jedna z tych klas (Teacher) posiada relację OneToMany z inną encją.

Potrzebowałem stworzyć metodę, która pozwoli mi na wyszukiwanie osób w repozytorium Person po różnych kryteriach – zarówno tych wspólnych, jak i specyficznych dla danej klasy. Wykorzystałem w tym celu Specification z JPA. Wstępnie wydawało się, że wszystko działa poprawnie – testy przechodziły, zapytania w Postmanie zwracały oczekiwane wyniki.

Problem pojawia się przy wyszukiwaniu osób w klasie, która posiada relację OneToMany, występuje wówczas n+1 zapytań. Zamiast jednego SELECT-a, który zwróciłby wszystkie potrzebne dane, dla każdej osoby z relacją generowane jest osobne zapytanie do bazy. Przykładowo, dla 1000 osób, z których 300 to nauczyciele posiadający przypisane klasy, zamiast jednego zapytania SQL, wykonuje się 1 + 300 zapytań.

Dla klas bez relacji wszystko działa prawidłowo – jedno zapytanie wystarcza, aby pobrać wszystkie dane. Natomiast dla klasy z relacją występuje problem n+1.

Gdyby klasa Person nie była abstrakcyjna, mógłbym łatwo rozwiązać ten problem, dodając do repozytorium odpowiednie metody findAll z LEFT JOIN FETCH oraz projekcję, aby od razu zwracać PersonDTO. Niestety, w przypadku klasy abstrakcyjnej, która łączy różne typy encji, sprawa się komplikuje.

Zastanawiam się nad zastosowaniem trzech oddzielnych metod findAll dla każdej z dziedziczących klas, a następnie scalaniem wyników w całość, ale obawiam się, że może to nie być najbardziej wydajne rozwiązanie. Czy ktoś spotkał się z podobnym problemem i ma jakieś sugestie, jak to efektywnie rozwiązać?

Będę wdzięczny za każdą wskazówkę.

Pozdrawiam! 😊

edytowany 1x, ostatnio: xwns
hzmzp
  • Rejestracja:ponad 11 lat
  • Ostatnio:około 4 godziny
  • Postów:632
1

Z tego co pamiętam to u mnie rozwiązywali to na 2 sposoby, pisali własny zoptymalizowany SQL albo próbowali dodać cascade = CascadeType.ALL i FetchType.EAGER.
Ja osobiście unikam jak ognia tego typu zapytań :)

MM
Z tego co wiem to FetchType.EAGER nie eliminuje n+1. Typ fetcha mówi tylko o tym kiedy dane mają być zfetchowane, a nie jak. Możesz je mieć sfetchowane eagerly i nadal mieć je w osobnych zapytaniach. Chyba że ta cascade coś zmienia ale nie rozumiem jak.
MM
Chociaż przecież entity graph rozwiązuje n+1, a pozwala właśnie na dynamiczne zmienianie fetch type. No ale z drugiej strony, np tutaj https://stackoverflow.com/questions/53938827/does-onetomanyfetch-fetchtype-eager-executes-n1-queries piszą tak jak ja w 1 komentarzu.
BB
  • Rejestracja:ponad 2 lata
  • Ostatnio:8 minut
  • Postów:67
0
XW
  • Rejestracja:ponad rok
  • Ostatnio:7 miesięcy
  • Postów:19
0
hzmzp napisał(a):

Z tego co pamiętam to u mnie rozwiązywali to na 2 sposoby, pisali własny zoptymalizowany SQL albo próbowali dodać cascade = CascadeType.ALL i FetchType.EAGER.
Ja osobiście unikam jak ognia tego typu zapytań :)

FetchType.EAGER zadziała niestety tylko przy wyszukiwaniu jednego obiektu za pomocą np. findById - wówczas pominie relacje i wyświetli prawidłowo.

Dla findAll nie działa.

XW
  • Rejestracja:ponad rok
  • Ostatnio:7 miesięcy
  • Postów:19
0
bbzzyyczczeek napisał(a):

https://marcinszewczyk.pl/problem-n-plus-1

We wspomnianym linku opisywany jest prosty przykład na zasadzie encja —OneToMany — encja.

Tutaj przypadek jest inny, mamy abstract class, więc zastosowanie join fetch na poziomie encji Person i metodzie findAll nie zadziała, bo naturalnie person nie ma tej relacji, tylko Teacher.

hzmzp
  • Rejestracja:ponad 11 lat
  • Ostatnio:około 4 godziny
  • Postów:632
0

@MckMaciek tak ja wspomniałem, były używane te adnotacje, albo pisaliśmy SQL bezpośrednio w Hibernate, raczej nie bawiliśmy się w CriteriaBuilder, ewentualnie robiliśmy widoki (SQL CREATE VIEW...) i z tego bezpośrednio do DTO. Mimo to tylko w przypadku gdy user musiał mieć te dane na żądanie. Jak system miał to gdzieś sobie na backend przetwarzać to nie było problemu, dłużej będzie się przetwarzać.

Szczerze to jeżeli jest inny sposób to chętnie się o tym dowiem. Może czegoś się nauczę :)

MM
Nie no ja rozumiem argumentację, zastanawiam się tylko jak to jest, że entity graph, czyli rozwiązanie, które powstało stricte żeby wyeliminować n+1, poprzez dynamiczna zmiane fetch type, ma się do tego, że sam fetch type eager przecież nie gwarantuje że n+1 nie będzie. Ale może się mylę. Fajnie jakby mnie ktoś wyprowadził z błędu.
MM
@jarekr000000: pardon, że tak bezpośrednio wywołuje ale masz może na to jakieś wytłumaczenie? Jesteś magikiem, nekromantą itd. to musisz wiedzieć
jarekr000000
@MckMaciek: chyba czas oddać tytuły, spalić szatę. Tak dawno już nie miałem do czynienia z JPA, że nie mam pojęcia. Co gorsza nie chce mi się nawet przypominać (JPA to jest niesmaczny żart).
PI
  • Rejestracja:ponad 9 lat
  • Ostatnio:5 miesięcy
  • Postów:2787
0
Korges
  • Rejestracja:około 5 lat
  • Ostatnio:około 3 godziny
  • Postów:571
XW
  • Rejestracja:ponad rok
  • Ostatnio:7 miesięcy
  • Postów:19
0
Pinek napisał(a):

https://vladmihalcea.com/n-plus-1-query-problem/

Korges napisał(a):

https://nullpointerexception.pl/hibernate-i-problem-n-plus-1-zapytan/

W podstawowym przypadku wiem jak poradzić sobie z n+1.

Ciekawi mnie, czy ktoś mądrzejszy ma wydajniejsza opcję niż rozdzielanie jednego findAll (person) na 3 różne dla danych typów klas.
Szukam rozwiązania, które pozwoli pozostać przy findAllu na abstract class pomijając relację w jednej z encji, która dziedziczy po Person

Możliwe, że nie ma takiego rozwiazania, ale z Waszych linków Panowie nic takiego nie wynika.
Mimo wszystko dziękuje za włączenie się do dyskusji. 😛

Korges
Jak sam zauważyłeś JPA klęka wszędzie tam gdzie dzieje się coś bardziej skompilowanego. Własna metoda @Query w repozytorium to nie jest nic nadzwyczajnego. Możesz w niej mieć jakieś casey i selecty połączone unionem. Wszystko się da w ramach jednego zapytania
PR
  • Rejestracja:prawie 4 lata
  • Ostatnio:2 minuty
  • Postów:222
0
  1. Ta kolekcja powinna być Lazy, jeśli tego nie potrzebujesz
  2. Customowe query, JPQL lub raw SQL
  3. Wariacja JPQL z entity graphem, zeby sciagnal to co chcesz typu (experimental)
Kopiuj
@EntityGraph(attributePaths = {"otherCollection"})
@Query("SELECT p FROM Person p")
List<Person> findAllWithOtherCollection();
  1. Trick: jeśli chcesz znaleźć półśrodek, możesz włączyć batchowanie kolekcji, takie pudrowanie nosa, ale często szybko pomagało z n+1 i było wystarczające. (uwaga na pamięć) -> default_batch_fetch_size = 10 lub 25
PI
  • Rejestracja:ponad 9 lat
  • Ostatnio:5 miesięcy
  • Postów:2787
0

Ja np nigdzie nie mam @OnetoMany, w ogóle kolekcji nie ma dana encja. Jest sobie jedynie

Kopiuj
Long parentId;

w encji dziecka. Czyli nawet w ogóle żadnego JPA nie zatrudniam do relacji :P Jak chcę wyciągnąć kolekcję dzieci po parencie, to robię

Kopiuj
childSpringJpaRepository.findAllByParentId(Long parentId);
edytowany 1x, ostatnio: Pinek
hzmzp
Ale ty rozumiesz że te subselecty lecą bo on chce te dane wyciągnąć?
PI
No to niech napisze gołego SQLa :P chociaż IMO bardziej bym się zastanowił nad designem bo to pachnie złym podejściem.
PR
pachnie typowym ORMem ;)

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.