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

Ответ

Ленивая загрузка (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 — предпочтительный выбор для производительности.

Ответ 18+ 🔞

А, слушай, про эту вашу ленивую загрузку в Hibernate! Ну, классика же, блядь. Это когда он тебе, такой хитрожопый, вместо того чтобы сразу всё из базы тащить, подсовывает какую-то хуйню-прокси, пустышку.

Представь: ты заказываешь пиццу (это твой Order). Привозят коробку. Ты открываешь — а там, блядь, не пицца, а бумажка: «Пицца будет, когда первый кусок возьмёшь». Вот эта бумажка — оно и есть, прокси-объект, сука! Пока ты не ткнешь в order.getItems() — нихуя не произойдёт, запроса в БД не будет. Экономия, оптимизация, мать её.

Вот смотри, как это выглядит в коде, тут всё чётко:

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

    // Вот эта штука по умолчанию уже ленивая, но лучше явно указать, чтоб не пиздец какой-то
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "order")
    private List<OrderItem> items; // Это не список, а обманка, блядь!
}

А теперь главная подстава, которая всех новичков накрывает медным тазом — LazyInitializationException. Это пиздец какой-то, в рот меня чих-пых!

Ситуация: ты получил заказ в открытой сессии, всё ок.

Order order = session.get(Order.class, 1L); // Всё гуд, запрос пошёл

А потом закрыл сессию и такой: «А покажи-ка мне состав заказа!»

session.close(); // Всё, конец связи с базой
order.getItems().size(); // БАБАХ! LazyInitializationException! Пизда!

Прокси-пустышка пытается сходить в базу за данными, а сессия-то уже прикрыта! Куда идти? Неизвестно! Вот он и орёт, сука, как резаный.

Как с этим жить, не сходя с ума:

  1. Hibernate.initialize(proxy) — это как взять эту бумажку-прокси и прямо при курьере сказать: «Ну-ка, быстро, пиццу сюда!» Пока он тут, пока сессия открыта.

    Hibernate.initialize(order.getItems()); // Готово, теперь в items реальные данные
  2. JOIN FETCH в запросе — это вообще красота. Ты с самого начала в одном запросе говоришь: «Привези мне заказ И ВСЕ ЕГО ПОЗИЦИИ СРАЗУ, НАХУЙ!»

    String jpql = "SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id";

    Никаких прокси, всё честно, сразу в объекте. Искусство, блядь!

  3. EntityGraph — это такая умная настройка, где ты заранее рисуешь, что с чем тащить. Как список покупок для Hibernate: «Возьми это, это, и вот это не забудь».

  4. Open Session in View (OSIV) — опасная, блядь, штука. Сессия висит открытой на весь HTTP-запрос. Удобно? Да. А потом оказывается, что ленивые загрузки срабатывают прямо в шаблонизаторе (типа Thymeleaf), и вроде работает. Но это, чувак, как ходить по охуенно тонкому льду. Можно не заметить, как наделаешь N+1 запросов и положишь приложение. Стратегия для опытных и очень аккуратных.

Итог, блядь:
Ставь везде LAZY по умолчанию, ебать мои старые костыли. EAGER — только если ты на 146% уверен, что эта связь нужна ВСЕГДА и ВМЕСТЕ. А то получишь ситуацию, когда загружаешь один заказ, а Hibernate тебе за компанию полбазы подтянул, потому что там @ManyToOne жадно настроен. А потом сидишь и думаешь: «И почему это у меня всё тормозит?» Да потому что ты, мудак, стратегию загрузки не продумал!

Вот так вот, коротко и без лишней ебли. Главное — помни про прокси и про то, что они требуют открытой сессии. Всё остальное — технические детали для решения этой ебучей головоломки.