Коли використовувати Lazy, а коли Eager loading
Вибір між LAZY та EAGER — це вибір між масштабованістю та зручністю розробки. У сучасній Java/Spring розробці існує "золоте правило": використовуйте LAZY за замовчуванням для пе...
Огляд
Вибір між LAZY та EAGER — це вибір між масштабованістю та зручністю розробки. У сучасній Java/Spring розробці існує “золоте правило”: використовуйте LAZY за замовчуванням для переважної більшості зв’язків.
🟢 Junior Level
Базове правило
Майже завжди використовуйте LAZY. EAGER — тільки коли точно знаєте навіщо.
LAZY коли:
- Зв’язок
@OneToManyабо@ManyToMany - Пов’язані дані не завжди потрібні
- Великі колекції
- Дані можуть бути важкими для завантаження
EAGER коли:
- Зв’язок
@ManyToOneі дані завжди потрібні - Маленькі, обов’язкові зв’язки (довідники)
- Дані потрібні в 100% випадків використання батька
// LAZY — items не завжди потрібні
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;
// LAZY — user не завжди потрібен
@ManyToOne(fetch = FetchType.LAZY)
private User user;
// EAGER має сенс лише для маленьких обов'язкових зв'язків
// UserProfile — маленька сутність, завжди відображається разом з User,
// без нього User неповноцінний. EAGER тут виправданий.
@OneToOne(fetch = FetchType.EAGER)
private UserProfile profile; // завжди потрібен з User
Коли LAZY — поганий вибір
- Batch-обробка коли все одно потрібні всі пов’язані дані — краще одразу завантажити через JOIN FETCH
- Маленькі довідники (1-10 записів) — overhead на lazy-завантаження перевищує користь
- Сутності які ЗАВЖДИ потрібні разом — два окремі запити повільніші одного JOIN
🟡 Middle Level
Практичні рекомендації
@ManyToOne → LAZY (майже завжди, навіть попри дефолт EAGER)
@OneToMany → LAZY (дефолт, не змінюйте)
@OneToOne → LAZY (часто забуваємо перевизначити)
@ManyToMany → LAZY (дефолт, не змінюйте)
Чому перевизначати @ManyToOne на LAZY
@Entity
public class Order {
// Дефолт — EAGER, але ми перевизначаємо
@ManyToOne(fetch = FetchType.LAZY)
private User user;
}
// При завантаженні Order без JOIN:
// Без LAZY: SELECT o.*, u.* FROM orders o JOIN users u ON ...
// З LAZY: SELECT o.* FROM orders o (user завантажиться лише при зверненні)
Приклади правильних рішень
// ✅ LAZY для колекції
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;
// ✅ LAZY для @ManyToOne
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
// ✅ LAZY для @OneToOne
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "address_id")
private Address address;
// Завантаження коли потрібне — через JOIN FETCH
@Query("SELECT DISTINCT o FROM Order o " +
"JOIN FETCH o.user " +
"JOIN FETCH o.items " +
"WHERE o.id = :id")
Order findByIdComplete(@Param("id") Long id);
Типові помилки
// ❌ EAGER для великих колекцій
@OneToMany(fetch = FetchType.EAGER)
private List<Order> orders;
// Завантажить ВСІ замовлення → деградація продуктивності
// ❌ EAGER "на всякий випадок"
@ManyToOne(fetch = FetchType.EAGER)
private Category category;
// Навіть коли категорія не відображається — зайвий JOIN
🔴 Senior Level
Архітектурні Trade-offs
| LAZY | EAGER |
|---|---|
| Гнучкіше та масштабованіше | Пріше у розробці |
| Потрібно думати про завантаження | Завжди доступно |
| Може викликати LazyInitializationException | Може викликати N+1 проблему |
| Динамічне завантаження | Статичне завантаження |
| Підходить для мікросервісів | Підходить для монолітів |
Стратегія Senior-розробника: “LAZY + динамічне завантаження”
Замість статичного оголошення FetchType в Entity, використовуйте динамічне завантаження:
1. Глобально: LAZY для всіх зв'язків
2. Локально: FETCH коли потрібно в конкретному запиті
- JPQL: JOIN FETCH
- Spring Data JPA: @EntityGraph
- Criteria API: root.fetch("association")
Чому “LAZY за замовчуванням” — це правильно
Якщо ви оголосите зв'язок як EAGER:
→ ви НІКОЛИ не зможете зробити його лінивим в конкретному запиті
→ Hibernate все одно підтягне дані
Якщо ви оголосите зв'язок як LAZY:
→ ви в будь-який момент можете "перетворити" його на EAGER через JOIN FETCH
→ LAZY дає максимальну гнучкість
DTO projection замість EAGER
// Замість EAGER завантажень — використовуйте DTO для API
@Query("""
SELECT new com.example.OrderDto(
o.id, o.date, o.status, u.name, u.email
)
FROM Order o JOIN o.user u
WHERE o.id = :id
""")
OrderDto findOrderDto(@Param("id") Long id);
// Переваги:
// 1. Один запит замість двох
// 2. Менше даних в пам'яті
// 3. Немає проблем з серіалізацією
// 4. Контрольований контракт API
Мікросервісна архітектура
В мікросервісах зі спільною БД (не рекомендується, але трапляється) LAZY особливо важливий, бо зайві дані = зайве навантаження на спільний ресурс. У повноцінних мікросервісах з окремими БД це питання вирішується на рівні API-контрактів.
Production Experience
// Spring Data JPA з @EntityGraph
public interface OrderRepository extends JpaRepository<Order, Long> {
@EntityGraph(attributePaths = {"user", "items"})
Optional<Order> findByIdWithDetails(Long id);
@EntityGraph(attributePaths = {"user"})
List<Order> findByUserId(Long userId);
// Без EntityGraph — тільки Order без зв'язків
List<Order> findByStatus(String status);
}
Best Practices
✅ LAZY за замовчуванням для ВСІХ зв'язків
✅ Перевизначення @ManyToOne та @OneToOne на LAZY
✅ JOIN FETCH коли дані потрібні
✅ @EntityGraph для динамічного завантаження
✅ DTO projection для read-only/API
✅ Моніторинг запитів до БД
❌ EAGER для колекцій
❌ EAGER "на всякий випадок"
❌ Вирішення LazyInitializationException через EAGER
❌ Завантаження сутностей для простих read-only операцій
Резюме для Senior
- EAGER — статичний і нав’язливий, його не можна скасувати
- LAZY — динамічний і гнучкий, можна завантажити коли потрібно
- Перевизначайте дефолтні EAGER-зв’язки (
OneToOne,ManyToOne) на LAZY - Вирішуйте проблему
LazyInitializationExceptionчерез правильне проектування транзакцій або Entity Graphs, а не через перехід на EAGER
🎯 Шпаргалка для співбесіди
Обов’язково знати:
- Золоте правило: LAZY за замовчуванням для ВСІХ зв’язків
- Перевизначати @ManyToOne та @OneToOne на LAZY (їх дефолт — EAGER)
- EAGER не можна скасувати в запиті, LAZY можна завантажити через JOIN FETCH
- LAZY дає максимальну гнучкість — динамічне завантаження коли потрібно
- DTO projection для API замість завантаження повних сутностей
- В мікросервісах LAZY особливо важливий для зниження навантаження на БД
Часті уточнюючі запитання:
- Коли EAGER виправданий? Маленькі обов’язкові зв’язки (довідники), дані потрібні в 100% випадків
- Чому перевизначати @ManyToOne на LAZY? Навіть коли user не потрібен — зайвий JOIN без LAZY
- Що робити з LazyInitializationException? Правильне проектування транзакцій, Entity Graphs, DTO — НЕ перехід на EAGER
Червоні прапорці (НЕ говорити):
- «EAGER для @ManyToOne — дефолт, не чіпаю» — потрібно перевизначати на LAZY
- «Вирішую LazyInitializationException через EAGER» — створює N+1
- «LAZY тільки для колекцій» — LAZY для всіх зв’язків включаючи @ManyToOne
Пов’язані теми:
- [[2. У чому різниця між Lazy та Eager завантаженням]]
- [[4. Що таке LazyInitializationException і як її уникнути]]
- [[5. Які стратегії fetch існують в Hibernate]]
- [[29. Що таке projection в JPA]]