Вопрос 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]]