Питання 3 · Розділ 16

Коли використовувати Lazy, а коли Eager loading

Вибір між LAZY та EAGER — це вибір між масштабованістю та зручністю розробки. У сучасній Java/Spring розробці існує "золоте правило": використовуйте LAZY за замовчуванням для пе...

Мовні версії: English Russian Ukrainian

Огляд

Вибір між 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 — поганий вибір

  1. Batch-обробка коли все одно потрібні всі пов’язані дані — краще одразу завантажити через JOIN FETCH
  2. Маленькі довідники (1-10 записів) — overhead на lazy-завантаження перевищує користь
  3. Сутності які ЗАВЖДИ потрібні разом — два окремі запити повільніші одного 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]]