Czasem najważniejsze ostrzeżenie produkcyjne wygląda niepozornie:
firstResult/maxResults specified with collection fetch; applying in memory!
Jeśli widzisz ten log, bardzo możliwe, że paginacja nie dzieje się w SQL, tylko w pamięci aplikacji. Najczęściej problem pojawia się przy połączeniu Pageable z JOIN FETCH na relacji One-to-Many.
Skąd bierze się problem
Przy dociąganiu kolekcji jeden rekord encji nadrzędnej może dać wiele wierszy w SQL. Jeden Author z pięcioma Book to pięć wierszy w wyniku. Ograniczenie liczby wierszy nie jest wtedy równoznaczne z ograniczeniem liczby encji nadrzędnych.
Hibernate nie jest w stanie bezpiecznie zagwarantować poprawnej paginacji na takim zapytaniu, więc ładuje więcej danych i przycina wynik w pamięci.
Częsty antywzorzec
@Query("select a from Author a left join fetch a.books order by a.id")
fun findPageWithBooks(pageable: Pageable): List<Author>
Wygodne, ale ryzykowne przy większym wolumenie danych.
Poprawny wzorzec: dwa kroki
Zamiast jednego zapytania użyj dwóch:
- Stronicuj same ID encji nadrzędnej.
- Dociągnij encje z relacją dla tej listy ID.
@Query("select a.id from Author a order by a.id")
fun findPageOfIds(pageable: Pageable): Page<Long>
@Query("""
select a
from Author a
left join fetch a.books
where a.id in :ids
order by a.id
""")
fun fetchByIdsWithBooks(@Param("ids") ids: List<Long>): List<Author>
W warstwie serwisowej sklej oba kroki w jedną odpowiedź.
Co to daje w praktyce
- Paginacja pozostaje deterministyczna.
- Zużycie pamięci jest przewidywalne.
- Nadal zwracasz pełny model z relacjami.
Szybka lista kontrolna do audytu
- Przeszukaj logi pod kątem komunikatu o in-memory pagination.
- Sprawdź repozytoria z
JOIN FETCHiPageable. - Zweryfikuj plan zapytania przy dużej kardynalności.
- Dodaj testy integracyjne na realistycznym wolumenie danych.
W systemach Java/Kotlin to jedna z najczęstszych ukrytych przyczyn regresji wydajności.
Potrzebujesz pomocy w identyfikacji problemów wydajnościowych? Dowiedz się więcej o naszych usługach audytu i modernizacji kodu.