В чём разница между Lazy и Eager загрузкой
FetchType определяет когда Hibernate загружает связанные сущности: сразу вместе с родительской (EAGER) или только при обращении (LAZY). Это фундаментальное решение, влияющее на...
Обзор
FetchType определяет когда Hibernate загружает связанные сущности: сразу вместе с родительской (EAGER) или только при обращении (LAZY). Это фундаментальное решение, влияющее на производительность и архитекку приложения.
Аналогия: Представьте, что вы загружаете книгу. EAGER — это сразу загрузить ещё и все рецензии, комментарии, историю изменений. LAZY — загрузить только книгу, а рецензии подтянуть когда пользователь нажмёт на вкладку.
🟢 Junior Level
Определения
LAZY (ленивая загрузка) — связанные данные загружаются только при первом обращении к ним.
EAGER (жадная загрузка) — связанные данные загружаются сразу вместе с основным объектом.
// EAGER — загружает items сразу
@OneToMany(fetch = FetchType.EAGER)
private List<OrderItem> items;
Order order = entityManager.find(Order.class, 1L);
// items УЖЕ загружен!
// LAZY — загружает items только при обращении
@OneToMany(fetch = FetchType.LAZY)
private List<OrderItem> items;
Order order = entityManager.find(Order.class, 1L);
// items ЕЩЁ НЕ загружен — это proxy
order.getItems().size(); // ТОЛЬКО ЗДЕСЬ происходит запрос к БД
Значения по умолчанию
| Аннотация | FetchType по умолчанию |
|---|---|
| @ManyToOne | EAGER |
| @OneToMany | LAZY |
| @OneToOne | EAGER |
| @ManyToMany | LAZY |
Значения по умолчанию указаны для JPA 2.x. В Hibernate 5/6 можно переопределить через @LazyToOne.
Когда что использовать
- LAZY — почти всегда, особенно для коллекций
- EAGER — только для маленьких обязательных связей, которые нужны в 100% случаев
🟡 Middle Level
LazyInitializationException
Самая частая проблема при работе с LAZY:
@Transactional
public Order getOrder(Long id) {
return entityManager.find(Order.class, id);
}
// Вне транзакции:
Order order = service.getOrder(1L);
order.getItems().size(); // ❌ LazyInitializationException!
// Сессия Hibernate уже закрыта, proxy не может загрузить данные
Решения
// 1. JOIN FETCH — загрузить в транзакции
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Order findByIdWithItems(@Param("id") Long id);
// 2. Hibernate.initialize() — принудительная инициализация
@Transactional
public Order getOrderWithItems(Long id) {
Order order = entityManager.find(Order.class, id);
Hibernate.initialize(order.getItems());
return order;
}
// 3. @EntityGraph — динамическая загрузка
@EntityGraph(attributePaths = {"items"})
Order findById(Long id);
// 4. Инициализация в сервисном слое
@Transactional(readOnly = true)
public OrderDto getOrderDto(Long id) {
Order order = repository.findById(id).orElseThrow();
order.getItems().size(); // force initialization
return OrderDto.from(order);
}
Open Session In View — антипаттерн
# ❌ Не рекомендуется в production
spring.jpa.open-in-view: true
Этот режим держит сессию открытой до завершения рендеринга view, что приводит к:
- Долгим транзакциям
- Утечкам соединений
- Скрытым проблемам производительности
Типичные ошибки
// ❌ EAGER для больших коллекций
@OneToMany(fetch = FetchType.EAGER)
private List<Order> orders;
// Загрузит ВСЕ заказы пользователя → OutOfMemoryError
// ❌ EAGER "на всякий случай"
@ManyToOne(fetch = FetchType.EAGER)
private User user;
// Даже когда user не нужен — лишний JOIN
🔴 Senior Level
Внутренняя реализация
Как работает LAZY loading:
1. Hibernate создаёт proxy для связанной сущности/коллекции
2. Для @ManyToOne — ByteBuddy proxy, расширяющий класс
ByteBuddy — библиотека для создания подклассов на лету. Hibernate использует её чтобы создать подкласс вашей сущности, который перехватывает обращения к полям и загружает данные из БД при первом обращении.
3. Для @OneToMany — PersistentCollection (PersistentBag/PersistentSet)
4. При обращении к proxy:
- Проверяет Session.isOpen()
- Если открыт → выполняет SELECT
- Если закрыт → LazyInitializationException
5. После загрузки proxy заменяется на реальный объект
Почему @ManyToOne по умолчанию EAGER:
@ManyToOne ссылается на ОДНУ сущность (например, Order -> User). Обычно вместе с заказом нужен и пользователь. @OneToMany ссылается на КОЛЛЕКЦИЮ (Order -> OrderItems), которая может содержать сотни записей — загружать их все всегда было бы расточительно.
Историческое решение в JPA спецификации, основанное на предположении, что родительская сущность обычно нужна вместе с дочерней. На практике это редко оправдано.
Архитектурные Trade-offs
| LAZY | EAGER |
|---|---|
| Гибче и производительнее | Проще в разработке |
| Требует планирования загрузки | Всегда доступно |
| Может вызвать LazyInitializationException | Может вызвать N+1 проблему |
| Подходит для высоконагруженных систем | Подходит для прототипов |
Стратегия “LAZY по умолчанию”
ПРАВИЛО: Все связи объявлять как LAZY, загружать явно когда нужно.
Почему это правильно:
1. EAGER нельзя отменить в конкретном запросе
2. LAZY можно "превратить" в EAGER через JOIN FETCH
3. LAZY даёт максимальную гибкость
@Entity
public class Order {
// Переопределяем дефолтный EAGER на LAZY
@ManyToOne(fetch = FetchType.LAZY)
private User user;
// LAZY — дефолт для коллекций
@OneToMany(mappedBy = "order")
private List<OrderItem> items;
}
DTO Projection вместо EAGER
// Вместо загрузки полных сущностей с EAGER связями
@Query("""
SELECT new com.example.OrderSummaryDto(
o.id, o.date, o.status, u.name, COUNT(i)
)
FROM Order o
JOIN o.user u
LEFT JOIN o.items i
WHERE o.id = :id
GROUP BY o.id, u.name
""")
OrderSummaryDto findOrderSummary(@Param("id") Long id);
Best Practices
✅ LAZY по умолчанию для подавляющего большинства связей. EAGER — только когда вы точно измерили и подтвердили, что связь нужна в 100% запросов.
✅ Переопределение @ManyToOne и @OneToOne на LAZY
✅ JOIN FETCH когда связанные данные нужны
✅ @EntityGraph для динамической загрузки
✅ DTO projection для API ответов
✅ Инициализация внутри @Transactional методов
❌ EAGER для коллекций (@OneToMany, @ManyToMany)
❌ Open Session In View в production
❌ Доступ к LAZY полям вне транзакции
❌ EAGER "на всякий случай"
🎯 Шпаргалка для интервью
Обязательно знать:
- LAZY — данные загружаются при первом обращении, EAGER — сразу
- По умолчанию: @ManyToOne = EAGER, @OneToMany = LAZY, @OneToOne = EAGER, @ManyToMany = LAZY
- LAZY по умолчанию — лучшая стратегия для всех связей
- EAGER нельзя отменить в запросе, LAZY можно «превратить» в EAGER через JOIN FETCH
- LAZY работает через proxy (ByteBuddy) и PersistentCollection
- LazyInitializationException возникает при доступе к LAZY вне транзакции
- DTO projection — лучшая альтернатива EAGER для API
Частые уточняющие вопросы:
- Почему LAZY лучше EAGER? LAZY гибок — можно загрузить когда нужно; EAGER навязчив — нельзя отменить
- Почему @ManyToOne по умолчанию EAGER? Историческое решение JPA — предполагалось что родительская сущность обычно нужна
- Как решить LazyInitializationException? JOIN FETCH, EntityGraph, инициализация внутри @Transactional, DTO projection
- Что такое Open Session In View? Сессия открыта до рендеринга view — антипаттерн для production
Красные флаги (НЕ говорить):
- «EAGER проще — использую его» — создаёт N+1 и проблемы с памятью
- «Решаю LazyInitializationException через EAGER» — лечит симптом, создаёт новую проблему
- «LAZY только для больших коллекций» — LAZY для ВСЕХ связей
- «Open Session In View — нормальная практика» — антипаттерн для production
Связанные темы:
- [[3. Когда использовать Lazy, а когда Eager loading]]
- [[4. Что такое LazyInitializationException и как её избежать]]
- [[1. Что такое проблема N+1 и как её решить]]
- [[5. Какие стратегии fetch существуют в Hibernate]]
- [[29. Что такое projection в JPA]]