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

You are currently viewing JPA – różnica między FetchType LAZY a EAGER

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ć, ż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 @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, ż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 OneToMany lub ManyToMany, 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 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!

Subscribe
Powiadom o
guest
0 komentarzy
najstarszy
najnowszy oceniany
Inline Feedbacks
View all comments