Как работает ленивая загрузка (Lazy Loading) в Hibernate?

«Как работает ленивая загрузка (Lazy Loading) в Hibernate?» — вопрос из категории Hibernate, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Ленивая загрузка (Lazy Loading) — это стратегия Hibernate, при которой связанные сущности или коллекции не загружаются из базы данных сразу при загрузке родительской сущности, а откладываются до момента первого обращения к ним. Это оптимизация по умолчанию для ассоциаций @OneToMany и @ManyToMany.

Как это реализовано: Hibernate возвращает не настоящую коллекцию или объект, а прокси-объект. При первом вызове метода геттера прокси инициирует загрузку реальных данных из БД.

Пример настройки:

@Entity
public class Order {
    @Id
    private Long id;

    // Ленивая загрузка коллекции (значение по умолчанию для @OneToMany)
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "order")
    private List<OrderItem> items; // Прокси, а не реальный список
}

@Entity
public class OrderItem {
    @Id
    private Long id;

    // Для @ManyToOne по умолчанию EAGER, но часто меняют на LAZY
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;
}

Типичное использование:

// Сессия открыта
Order order = session.get(Order.class, 1L); // SELECT * FROM order WHERE id=1
// Пока обращений к items нет, запроса не происходит.

System.out.println(order.getItems().size()); // Тут выполняется: SELECT * FROM order_item WHERE order_id=1

Критически важное ограничение — LazyInitializationException: Это исключение возникает при попытке ленивой загрузки вне контекста открытой сессии Hibernate (Session) или транзакции (в зависимости от конфигурации).

Order order = session.get(Order.class, 1L);
session.close(); // Сессия закрыта
order.getItems().size(); // LazyInitializationException!

Способы решения проблемы:

  1. Инициализация внутри сессии: Использовать Hibernate.initialize(proxy).
  2. JOIN FETCH в запросе: Явно загрузить данные в одном запросе.
    String jpql = "SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id";
  3. EntityGraph: Задать граф загрузки сущности.
  4. Паттерн Open Session in View (OSIV): Держать сессию открытой на время всего HTTP-запроса (имеет свои недостатки и требует аккуратной настройки).

Выбор стратегии: Используйте FetchType.EAGER только для связей, которые всегда нужны вместе с родительской сущностью. В остальных случаях LAZY — предпочтительный выбор для производительности.