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ć, że 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
@OneToManyi@ManyToManyużywająFetchType.LAZY. - Natomiast adnotacje
@OneToOnei@ManyToOneużywająFetchType.EAGER.
Nie musisz więc za każdym razem pisać fetch = .... Wystarczy, że 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śli 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, nawet 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śli 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
OneToManylubManyToMany, które mogą zawierać dużą liczbę encji, albo myślisz, że 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
OneToOnelubManyToOnejest 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!