У чому різниця між Lazy та Eager завантаженням
FetchType визначає коли Hibernate завантажує пов'язані сутності: одразу разом з батьківською (EAGER) або лише при зверненні (LAZY). Це фундаментальне рішення, що впливає на прод...
Огляд
FetchType визначає коли Hibernate завантажує пов’язані сутності: одразу разом з батьківською (EAGER) або лише при зверненні (LAZY). Це фундаментальне рішення, що впливає на продуктивність та архітектуру додатку.
Аналогія: Уявіть, що ви завантажуєте книгу. EAGER — це одразу завантажити ще й усі рецензії, коментарі, історію змін. LAZY — завантажити лише книгу, а рецензії підтягнути коли користувач натисне на вкладку.
🟢 Junior Level
Визначення
LAZY (ліниве завантаження) — пов’язані дані завантажуються лише при першому зверненні до них.
EAGER (жадібне завантаження) — пов’язані дані завантажуються одразу разом з основним об’єктом.
// EAGER — завантажує items одразу
@OneToMany(fetch = FetchType.EAGER)
private List<OrderItem> items;
Order order = entityManager.find(Order.class, 1L);
// items ВЖЕ завантажений!
// LAZY — завантажує items лише при зверненні
@OneToMany(fetch = FetchType.LAZY)
private List<OrderItem> items;
Order order = entityManager.find(Order.class, 1L);
// items ЩЕ НЕ завантажений — це proxy
order.getItems().size(); // ЛИШЕ ТУТ відбувається запит до БД
Значення за замовчуванням
| Анотація | FetchType за замовчуванням |
|---|---|
| @ManyToOne | EAGER |
| @OneToMany | LAZY |
| @OneToOne | EAGER |
| @ManyToMany | LAZY |
Значення за замовчуванням вказані для JPA 2.x. В Hibernate 5/6 можна перевизначити через @LazyToOne.
Коли що використовувати
- LAZY — майже завжди, особливо для колекцій
- EAGER — лише для маленьких обов’язкових зв’язків, які потрібні в 100% випадків
🟡 Middle Level
LazyInitializationException
Найчастіша проблема при роботі з LAZY:
@Transactional
public Order getOrder(Long id) {
return entityManager.find(Order.class, id);
}
// Поза транзакцією:
Order order = service.getOrder(1L);
order.getItems().size(); // ❌ LazyInitializationException!
// Сесія Hibernate вже закрита, proxy не може завантажити дані
Рішення
// 1. JOIN FETCH — завантажити в транзакції
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Order findByIdWithItems(@Param("id") Long id);
// 2. Hibernate.initialize() — примусова ініціалізація
@Transactional
public Order getOrderWithItems(Long id) {
Order order = entityManager.find(Order.class, id);
Hibernate.initialize(order.getItems());
return order;
}
// 3. @EntityGraph — динамічне завантаження
@EntityGraph(attributePaths = {"items"})
Order findById(Long id);
// 4. Ініціалізація в сервісному шарі
@Transactional(readOnly = true)
public OrderDto getOrderDto(Long id) {
Order order = repository.findById(id).orElseThrow();
order.getItems().size(); // force initialization
return OrderDto.from(order);
}
Open Session In View — антипатерн
# ❌ Не рекомендується в production
spring.jpa.open-in-view: true
Цей режим тримає сесію відкритою до завершення рендерингу view, що призводить до:
- Довгих транзакцій
- Витоків з’єднань
- Прихованих проблем продуктивності
Типові помилки
// ❌ EAGER для великих колекцій
@OneToMany(fetch = FetchType.EAGER)
private List<Order> orders;
// Завантажить ВСІ замовлення користувача → OutOfMemoryError
// ❌ EAGER "на всякий випадок"
@ManyToOne(fetch = FetchType.EAGER)
private User user;
// Навіть коли user не потрібен — зайвий JOIN
🔴 Senior Level
Внутрішня реалізація
Як працює LAZY loading:
1. Hibernate створює proxy для пов'язаної сутності/колекції
2. Для @ManyToOne — ByteBuddy proxy, що розширює клас
ByteBuddy — бібліотека для створення підкласів на льоту. Hibernate використовує її щоб створити підклас вашої сутності, який перехоплює звернення до полів і завантажує дані з БД при першому зверненні.
3. Для @OneToMany — PersistentCollection (PersistentBag/PersistentSet)
4. При зверненні до proxy:
- Перевіряє Session.isOpen()
- Якщо відкритий → виконує SELECT
- Якщо закритий → LazyInitializationException
5. Після завантаження proxy замінюється на реальний об'єкт
Чому @ManyToOne за замовчуванням EAGER:
@ManyToOne посилається на ОДНУ сутність (наприклад, Order -> User). Зазвичай разом із замовленням потрібен і користувач. @OneToMany посилається на КОЛЕКЦІЮ (Order -> OrderItems), яка може містити сотні записів — завантажувати їх усі завжди було б марнотратно.
Історичне рішення в JPA специфікації, засноване на припущенні, що батьківська сутність зазвичай потрібна разом з дочірньою. На практиці це рідко виправдано.
Архітектурні Trade-offs
| LAZY | EAGER |
|---|---|
| Гнучкіше і продуктивніше | Пріше у розробці |
| Вимагає планування завантаження | Завжди доступно |
| Може викликати LazyInitializationException | Може викликати N+1 проблему |
| Підходить для високонавантажених систем | Підходить для прототипів |
Стратегія “LAZY за замовчуванням”
ПРАВИЛО: Всі зв'язки оголошувати як LAZY, завантажувати явно коли потрібно.
Чому це правильно:
1. EAGER не можна скасувати в конкретному запиті
2. LAZY можна "перетворити" на EAGER через JOIN FETCH
3. LAZY дає максимальну гнучкість
@Entity
public class Order {
// Перевизначаємо дефолтний EAGER на LAZY
@ManyToOne(fetch = FetchType.LAZY)
private User user;
// LAZY — дефолт для колекцій
@OneToMany(mappedBy = "order")
private List<OrderItem> items;
}
DTO Projection замість EAGER
// Замість завантаження повних сутностей з EAGER зв'язками
@Query("""
SELECT new com.example.OrderSummaryDto(
o.id, o.date, o.status, u.name, COUNT(i)
)
FROM Order o
JOIN o.user u
LEFT JOIN o.items i
WHERE o.id = :id
GROUP BY o.id, u.name
""")
OrderSummaryDto findOrderSummary(@Param("id") Long id);
Best Practices
✅ LAZY за замовчуванням для переважної більшості зв'язків. EAGER — лише коли ви точно виміряли і підтвердили, що зв'язок потрібен в 100% запитів.
✅ Перевизначення @ManyToOne та @OneToOne на LAZY
✅ JOIN FETCH коли пов'язані дані потрібні
✅ @EntityGraph для динамічного завантаження
✅ DTO projection для відповідей API
✅ Ініціалізація всередині @Transactional методів
❌ EAGER для колекцій (@OneToMany, @ManyToMany)
❌ Open Session In View в production
❌ Доступ до LAZY полів поза транзакцією
❌ EAGER "на всякий випадок"
🎯 Шпаргалка для співбесіди
Обов’язково знати:
- LAZY — дані завантажуються при першому зверненні, EAGER — одразу
- За замовчуванням: @ManyToOne = EAGER, @OneToMany = LAZY, @OneToOne = EAGER, @ManyToMany = LAZY
- LAZY за замовчуванням — найкраща стратегія для всіх зв’язків
- EAGER не можна скасувати в запиті, LAZY можна «перетворити» на EAGER через JOIN FETCH
- LAZY працює через proxy (ByteBuddy) та PersistentCollection
- LazyInitializationException виникає при доступі до LAZY поза транзакцією
- DTO projection — найкраща альтернатива EAGER для API
Часті уточнюючі запитання:
- Чому LAZY краще EAGER? LAZY гнучкий — можна завантажити коли потрібно; EAGER нав’язливий — не можна скасувати
- Чому @ManyToOne за замовчуванням EAGER? Історичне рішення JPA — передбачалось що батьківська сутність зазвичай потрібна
- Як вирішити LazyInitializationException? JOIN FETCH, EntityGraph, ініціалізація всередині @Transactional, DTO projection
- Що таке Open Session In View? Сесія відкрита до рендерингу view — антипатерн для production
Червоні прапорці (НЕ говорити):
- «EAGER простіше — використовую його» — створює N+1 і проблеми з пам’яттю
- «Вирішую LazyInitializationException через EAGER» — лікує симптом, створює нову проблему
- «LAZY тільки для великих колекцій» — LAZY для ВСІХ зв’язків
- «Open Session In View — нормальна практика» — антипатерн для production
Пов’язані теми:
- [[3. Коли використовувати Lazy, а коли Eager loading]]
- [[4. Що таке LazyInitializationException і як її уникнути]]
- [[1. Що таке проблема N+1 і як її вирішити]]
- [[5. Які стратегії fetch існують в Hibernate]]
- [[29. Що таке projection в JPA]]