Wstęp
W tym wpisie z serii „Szybki strzał” bierzemy na celownik dwa sposoby ładowania danych w JPA (Java Persistence API): LAZY i EAGER. To nie tylko techniczny szczegół, ale istotna decyzja projektowa. Ma to realny wpływ na wydajność aplikacji i obciążenie bazy danych, zwłaszcza przy pracy z dużymi zbiorami danych. Przyjrzyjmy się więc, czym różnią się te podejścia i kiedy które z nich warto zastosować.
Co to jest FetchType w JPA?
W JPA każda relacja między encjami może być ładowana na dwa sposoby:
- LAZY (leniwe ładowanie) – dane są pobierane tylko wtedy, gdy są faktycznie potrzebne.
- EAGER (natychmiastowe ładowanie) – wszystkie powiązane dane są ładowane natychmiast przy pobraniu encji.
A jak to wygląda w kodzie? Sposób ładowania danych określamy z użyciem adnotacji i parametru fetch:
// Ustawienie LAZY @Entity public class User { @OneToMany(fetch = FetchType.LAZY) private List<Order> orders; } // Ustawienie EAGER @Entity public class User { @OneToMany(fetch = FetchType.EAGER) private List<Order> orders; }Warto też pamiętać, iż JPA domyślnie przypisuje strategię ładowania danych w zależności od typu relacji. Dlatego parametr fetch ustawiamy jawnie tylko wtedy, gdy chcemy zmienić domyślne zachowanie.
- Adnotacje @OneToMany i @ManyToMany używają FetchType.LAZY.
- Natomiast adnotacje @OneToOne i @ManyToOne używają FetchType.EAGER.
Nie musisz więc za każdym razem pisać fetch = .... Wystarczy, iż robisz to wtedy, gdy potrzebujesz innego sposobu ładowania niż ten domyślny.
@Entity public class Order { @ManyToOne // domyślnie FetchType.EAGER - nie musimy ustawiać parametru fetch private User user; } // Natomiast oczywiście możemy sobie nadpisać wartość na LAZY @Entity public class Order { @ManyToOne(fetch = FetchType.LAZY) // nadpisanie domyślnej wartości private User user; }FetchType LAZY – ładowanie leniwe
LAZY to strategia, w której powiązane encje są ładowane dopiero wtedy, gdy próbujemy uzyskać do nich dostęp. Innymi słowy, JPA „leniwie” odkłada pobieranie tych danych do momentu, kiedy faktycznie są one potrzebne.
@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String nazwa; @OneToMany // domyślnie LAZY private List<Order> orders; // gettery i setter }W praktyce wygląda to tak: pobierając użytkownika, zostaną załadowane tylko pola id i nazwa. Lista orders nie zostanie pobrana od razu – dopiero w momencie, gdy spróbujemy się do niej odwołać, JPA wykona dodatkowe zapytanie do bazy.
public class Main { public static void main(String[] args) { EntityManagerFactory emf = Persistence.createEntityManagerFactory("mojePU"); EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); User user = em.find(User.class, 1L); System.out.println("ID: " + user.getId()); System.out.println("Nazwa: " + user.getNazwa()); // Dopiero tutaj JPA zaciąga zamówienia (orders) // Wykonywany jest tutaj kolejny SELECT na bazie do pobrania zamówień! System.out.println("Zamówień: " + user.getOrders().size()); em.getTransaction().commit(); em.close(); emf.close(); } }Jeśli nie odwołasz się do getOrders(), żadne dodatkowe zapytanie nie poleci do bazy – to jest ta kluczowa cecha FetchType.LAZY.
Zalety:
- Mniejsze początkowe zapytanie do bazy – pobieramy tylko to, co na pewno będzie potrzebne
- Mniejsze zużycie pamięci, jeżeli nie potrzebujemy powiązanych encji
- Szybsze wykonanie początkowego zapytania
Wady:
- Możliwość wystąpienia LazyInitializationException, gdy próbujemy uzyskać dostęp do powiązanych encji po zamknięciu sesji
- Potencjalnie problem N+1 zapytań, gdy faktycznie potrzebujemy powiązanych danych
FetchType EAGER – ładowanie zachłanne
EAGER to przeciwieństwo LAZY. W tej strategii wszystkie powiązane encje są ładowane natychmiast wraz z encją główną, bez względu na to czy faktycznie będziemy z nich korzystać.
Jak spojrzymy na ten sam przykład co dla LAZY, ale zmienimy mu sposób zaciągania danych na EAGER to zobaczymy istotną zmianę w działaniu aplikacji.
@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String nazwa; @OneToMany(fetch = FetchType.EAGER) private List<Order> orders; // gettery i setter } public class Main { public static void main(String[] args) { // ... Przygotowanie EntityManager i otwarcie transakcji // Tutaj zaciągamy dane o User wraz z orders // Przy dużych zbiorach danych może się dłużej ten fragment kodu wykonywać User user = em.find(User.class, 1L); System.out.println("ID: " + user.getId()); System.out.println("Nazwa: " + user.getNazwa()); // Tutaj już nie korzystamy z JPA bo od razu mamy wszystkie dane zaciągnięte i pobieramy już dane ze zwykłego obiektu System.out.println("Zamówień: " + user.getOrders().size()); // ... Zamknięcie transakcji i EntityManager } }Zalety:
- Brak LazyInitializationException
- Dane są zawsze dostępne, choćby po zamknięciu sesji
- Prostszy kod (nie wymaga dodatkowych zabiegów do pobierania powiązanych encji)
Wady:
- Większe początkowe zapytanie do bazy
- Niepotrzebne obciążenie pamięci, jeżeli nie korzystamy z powiązanych encji
- Potencjalna kaskada zapytań przy złożonych relacjach
Kiedy stosować FetchType.LAZY, a kiedy FetchType.EAGER?
Wybór odpowiedniej strategii zależy od konkretnego przypadku użycia. Zanim zdecydujesz, spójrz całościowo na to, co dokładnie chcesz osiągnąć w danym fragmencie aplikacji, łatwiej będzie dobrać odpowiedni sposób zaciągania danych. Natomiast poniżej spisałem kilka rad, co w jakiej sytuacji najlepiej byłoby użyć.
LAZY warto używać, gdy:
- Zależy Ci na optymalnej wydajności
- Pracujesz z relacjami OneToMany lub ManyToMany, które mogą zawierać dużą liczbę encji, albo myślisz, iż projektowo mogą w przyszłości być dużymi zbiorami danych
- Nie zawsze potrzebujesz dostępu do powiązanych danych
Natomiast EAGER, ma sens gdy:
- Relacja OneToOne lub ManyToOne jest z niewielką liczbą danych
- Niemal zawsze potrzebujesz dostępu do powiązanych encji
- Zależy Ci na prostocie, a nie na optymalnej wydajności
Podsumowanie
Różnica między FetchType.LAZY a EAGER w JPA sprowadza się do momentu, w którym dane są ładowane z bazy. LAZY opóźnia pobieranie do czasu faktycznego użycia, co zwykle pomaga zoptymalizować wydajność, ale wymaga ostrożności z sesją Hibernate. EAGER ładuje wszystko od razu, co jest prostsze w użyciu, ale może powodować niepotrzebne obciążenie.
W kolejnych wpisach możemy skupić się na omówieniu wszystkich 4 sposobów łączenia encji przez adnotacje, albo też możemy omówić temat problemu zaciągania danych N+1 dla leniwego ładowania danych. Daj znać w komentarzy czy takie tematy by Ciebie interesowały!