← назад к разделу

Hibernate позволяет не загружать связанные объекты из базы сразу — а подтянуть их позже, когда они реально понадобятся. Это удобно, но легко сделать неправильно и получить либо лишние запросы, либо загадочную ошибку. Разберём оба режима и типичные ловушки.

Два режима загрузки

При маппинге связей через @OneToMany, @ManyToOne, @OneToOne или @ManyToMany можно указать fetch:

@Entity
public class Order {

    @Id
    private Long id;

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

    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    private List<OrderItem> items;
}

FetchType.EAGER — связанный объект загружается сразу вместе с родительским, одним запросом (или join'ом).

FetchType.LAZY — связь не загружается до первого обращения к ней. Hibernate подставляет вместо неё прокси — пустой объект-заглушку, который знает только идентификатор. Реальный SQL выполняется позже, когда вы первый раз вызовете любой метод этого объекта.

Короткая формула: LAZY — «грузи когда попросят», EAGER — «грузи сразу».

Как работает прокси

Для одиночных связей (@ManyToOne, @OneToOne) Hibernate создаёт подкласс-прокси вашей сущности. Внешне он ничем не отличается от обычного объекта:

Order order = entityManager.find(Order.class, 1L);
// SQL: SELECT * FROM orders WHERE id = 1
// customer ещё НЕ загружен

String name = order.getCustomer().getName();
// вот здесь Hibernate выполняет второй SELECT * FROM customers WHERE id = ?

Для коллекций (@OneToMany, @ManyToMany) вместо List или Set Hibernate подставляет свою реализацию — PersistentBag, PersistentSet и другие. Они тоже пустые до первого обращения.

LazyInitializationException: что это и почему возникает

Это самая частая ошибка при работе с ленивой загрузкой:

org.hibernate.LazyInitializationException:
  failed to lazily initialize a collection of role: Order.items,
  could not initialize proxy - no Session

Причина — вы обратились к ленивой связи после закрытия сессии Hibernate. Прокси знает, что нужно загрузить данные, но открытого соединения с базой уже нет.

Типичный сценарий в Spring:

// Транзакция открыта — загружаем Order
@Transactional
public Order getOrder(Long id) {
    return orderRepository.findById(id).orElseThrow();
}   // <-- транзакция закрыта здесь

// Позже, вне транзакции:
Order order = orderService.getOrder(1L);
order.getItems().size(); // ВЗРЫВ: LazyInitializationException

Сессия Hibernate живёт ровно в рамках транзакции. Когда метод с @Transactional завершился — сессия закрыта, прокси стал «мёртвым».

Как правильно лечить

1. Загружать нужные данные в транзакции через JOIN FETCH

Самый чистый способ — явно описать, что нужно загрузить, прямо в запросе:

@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") Long id);

Hibernate выполнит один SQL с JOIN и вернёт полностью инициализированный объект. Ленивая связь уже не нужна — данные есть.

2. Загружать через EntityGraph

Альтернатива JPQL-запросу — @EntityGraph из Spring Data JPA:

@EntityGraph(attributePaths = {"items"})
Optional<Order> findById(Long id);

Гибкий вариант, когда нужно переиспользовать один и тот же метод репозитория с разными наборами связей. Подробнее про репозитории — в статье Spring Data JPA.

3. Обращаться к связям внутри транзакции

Если бизнес-логика требует данных из связи — пусть это происходит в рамках той же транзакции:

@Transactional
public int countItems(Long orderId) {
    Order order = orderRepository.findById(orderId).orElseThrow();
    return order.getItems().size(); // OK: сессия ещё открыта
}

Почему не стоит везде ставить EAGER

Первый порыв — «поставлю EAGER и забуду про ошибку». Это ловушка.

Проблема 1: лишние данные всегда. Даже когда вам нужен только идентификатор заказа, Hibernate загрузит все его позиции из базы — потому что EAGER означает «всегда».

Проблема 2: N+1 запросов или декартово произведение. Если у вас список из 100 заказов и у каждого EAGER-коллекция позиций — Hibernate либо выполнит 100 дополнительных SELECT (N+1), либо сделает JOIN и вернёт строки с повторами. Подробнее про N+1 — в статье N+1 проблема в Hibernate.

Проблема 3: неожиданные цепочки. EAGER на одной связи может потянуть EAGER-цепочку дальше по графу, и в итоге один find() загрузит половину базы.

Правило: оставляйте LAZY по умолчанию, а что именно нужно — указывайте в запросе явно.

Справка по умолчаниям JPA:

АннотацияУмолчание
@ManyToOneEAGER
@OneToOneEAGER
@OneToManyLAZY
@ManyToManyLAZY

@ManyToOne и @OneToOne по умолчанию EAGER — исторически так сложилось. Рекомендуется явно менять на LAZY:

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

Open Session in View: не полагайтесь на него

В Spring Boot по умолчанию включён Open Session in View (OSIV) — паттерн, который держит сессию Hibernate открытой на всё время HTTP-запроса, включая рендеринг шаблона.

Это позволяет обращаться к ленивым связям даже за пределами @Transactional — ошибки нет, и кажется, что «всё работает». Но за это платите скрытыми SQL-запросами в слое представления: контроллер или шаблон, получая order.getItems(), незаметно идёт в базу.

Чтобы отключить OSIV:

spring:
  jpa:
    open-in-view: false

После отключения LazyInitializationException начнут вылезать там, где данные не были загружены явно — это хорошо, потому что делает поведение явным. Загружайте нужные связи в транзакции, а не полагайтесь на открытую сессию как «страховку».

Коротко

  • FetchType.LAZY — Hibernate подставляет прокси и грузит данные при первом обращении; EAGER — грузит сразу.
  • @ManyToOne и @OneToOne по умолчанию EAGER — явно меняйте на LAZY.
  • LazyInitializationException возникает при обращении к прокси после закрытия сессии (вне транзакции).
  • Правильное лечение — загрузить нужные связи внутри транзакции: через JOIN FETCH в JPQL или @EntityGraph.
  • Не ставьте EAGER везде: это скрытые лишние запросы и риск N+1.
  • OSIV скрывает проблему, но не решает её — отключайте и загружайте явно.

Что почитать дальше

  • N+1 проблема в Hibernate — как ленивая загрузка в цикле порождает лавину запросов и как её обнаружить.
  • Persistence context и жизненный цикл сущности — как сессия Hibernate управляет состоянием объектов.
  • Типичные ошибки с Hibernate — частые грабли при работе с ORM.
  • Spring Data JPA — репозитории, @EntityGraph и другие абстракции поверх Hibernate.