JPA – różnica między FetchType LAZY a EAGER

uprogramisty.pl 1 tydzień temu

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:

  1. LAZY (leniwe ładowanie) – dane są pobierane tylko wtedy, gdy są faktycznie potrzebne.
  2. 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!

Idź do oryginalnego materiału