JavaRush /Курси /Модуль 5. Spring /Ліниве та жадібне завантаження даних (Lazy і Eager Fetchi...

Ліниве та жадібне завантаження даних (Lazy і Eager Fetching)

Модуль 5. Spring
Рівень 5 , Лекція 9
Відкрита

Робота з об'єктами в JPA дозволяє нам забути про "брудну" роботу з SQL-запитами і оперувати звичними Java-об'єктами. Однак при вибірці пов'язаних даних постає важливе питання: коли їх підвантажувати? Для цього в JPA є два підходи: Lazy (ліниве) і Eager (жадібне) завантаження.

Як це працює на практиці?

Уявіть сервіс замовлень, де в кожного замовлення є список товарів:

  • Жадібне завантаження (Eager) — коли ви запитуєте замовлення, JPA автоматично підвантажить всі пов'язані товари. Зручно, але якщо у вас тисяча замовлень і потрібні тільки їхні номери — отримаєте величезний обсяг зайвих даних.
  • Ліниве завантаження (Lazy) — JPA підвантажить тільки основну інформацію про замовлення. Пов'язані товари завантажаться тільки коли ви явно до них звернетесь. Економно, але треба уважно ставитися до моменту підвантаження даних.

Вибір між Lazy і Eager — це завжди компроміс між зручністю і продуктивністю. І часто правильний вибір залежить від конкретного сценарію використання.

Конфігурації типів завантаження

JPA пропонує два способи завантаження пов'язаних даних, і ви можете вибрати підходящий за допомогою анотації fetch.

Анотація fetch в JPA


@ManyToOne(fetch = FetchType.LAZY)
private User user;
  • FetchType.LAZY — "ліниве" завантаження: дані підтягуються тільки коли вони реально потрібні.
  • FetchType.EAGER — "жадібне" завантаження: всі пов'язані дані завантажуються одразу.

Розглянемо реальний приклад інтернет-магазину:


@Entity
public class Order {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Customer customer;

    @OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
    private List<Product> products;

    // геттери і сеттери
}

Що тут відбувається:

  • Інформація про покупця (Customer) завантажується ліниво. Логічно: нам не завжди потрібні дані клієнта при роботі з замовленням.
  • А от товари (Product) завантажуються одразу — адже замовлення без списку товарів не має сенсу!

Різниці між Lazy і Eager

Lazy Loading Eager Loading
Коли підвантажує дані Коли до них звертаються Одразу при завантаженні батьківського об'єкта
Продуктивність Економить ресурси, якщо пов'язані дані не використовуються Може призводити до надмірних запитів
Складність реалізації Можуть виникнути помилки типу LazyInitializationException Все працює "з коробки"
Використання Рекомендується для більшості операцій Використовується тільки якщо впевнені, що дані потрібні завжди

Практика: Ліниве завантаження в дії

Приклад сутностей: Клієнт (Customer) і Замовлення (Order)

Давайте трохи ускладнимо наш додаток для роботи з базою даних. У нас вже є сутності Customer і Order, і ми реалізуємо зв'язок "один-до-багатьох" між клієнтом і його замовленнями.

Сутність Customer


@Entity
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "customer", fetch = FetchType.LAZY)
    private List<Order> orders = new ArrayList<>();

    // геттери і сеттери
}

Сутність Order


@Entity
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String description;

    @ManyToOne(fetch = FetchType.LAZY)
    private Customer customer;

    // геттери і сеттери
}

Ми бачимо, що обидві сторони налаштовані на Lazy. Тепер давайте спробуємо отримати дані з бази.

Репозиторії


@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> { }

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> { }

Сервісний клас


@Service
public class CustomerService {

    @Autowired
    private CustomerRepository customerRepository;

    public void printCustomerOrders(Long customerId) {
        Customer customer = customerRepository.findById(customerId).orElseThrow();

        System.out.println("Клієнт: " + customer.getName());
        System.out.println("Замовлення: " + customer.getOrders());
    }
}

Помилка LazyInitializationException

Спробуйте викликати метод printCustomerOrders(), і... БАААМ! Помилка!

LazyInitializationException каже нам, що контекст Hibernate вже закрив з'єднання з базою, і ми не можемо завантажити ліниві дані. Причина? Ми намагаємося звернутися до customer.getOrders() вже за межами транзакції.

Як уникнути цієї помилки?

  1. Використовувати Eager завантаження. Просто змінити fetch на FetchType.EAGER. Але це не завжди виправдано.
  2. Ручне підвантаження даних. Використовувати метод Hibernate.initialize() для завантаження лінивих даних.
    
    @Transactional
    public void printCustomerOrders(Long customerId) {
        Customer customer = customerRepository.findById(customerId).orElseThrow();
        Hibernate.initialize(customer.getOrders());
        System.out.println("Клієнт: " + customer.getName());
        System.out.println("Замовлення: " + customer.getOrders());
    }
    
  3. Або (мій улюблений спосіб): JOIN FETCH. Використовувати запит з JOIN FETCH для підвантаження даних заздалегідь.
    
    @Query("SELECT c FROM Customer c JOIN FETCH c.orders WHERE c.id = :id")
    Customer findCustomerWithOrders(@Param("id") Long id);
    

    І в методі просто:

    
    public void printCustomerOrders(Long customerId) {
        Customer customer = customerRepository.findCustomerWithOrders(customerId);
        System.out.println("Клієнт: " + customer.getName());
        System.out.println("Замовлення: " + customer.getOrders());
    }
    

Чому це так важливо?

Коли ви проєктуєте складні додатки, тип завантаження даних може суттєво впливати на продуктивність. Наприклад, якщо в системі тисячі сутностей, жадібне завантаження може просто "потопити" базу даних під тоннами зайвих запитів. З іншого боку, неправильно налаштована лінива поведінка може привести до маси неочікуваних проблем під час виконання.


Типові помилки і як їх уникнути

  • Зловживання Eager завантаженням: зазвичай це помилка новачків. Вони думають, що "чим більше даних підвантажу — тим краще". Але якщо дані не використовуються, це марна трата ресурсів.
  • Відсутність транзакції при Lazy завантаженні: переконайтеся, що ліниві дані завжди підвантажуються в рамках транзакції.
  • Перевантаження запитами при Lazy завантаженні: якщо ліниві дані підвантажуються в циклі, це може породити безліч окремих SQL-запитів. Використовуйте JOIN FETCH, щоб об'єднати їх в один запит.

Практичне застосування

На співбесіді вас обов'язково запитають про різницю між Lazy і Eager. Знання, як уникнути помилок лінивості, а також навіщо і де застосовувати жадібність (у хорошому сенсі) — це ваш шанс справити враження.

У реальних проєктах, де взаємодія з великою кількістю пов'язаних даних неминуча, правильна настройка завантаження критично важлива для продуктивності. Наприклад, соціальні мережі, де користувачі пов'язані з масою даних — від друзів до постів і коментарів, потребують грамотного вибору механізму завантаження.


Тож даваймо рухатися далі і не лінуватися, щоб не було лінивих виключень!

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ