Этап 7 - Fetch Types: LAZY, EAGER и проблемы загрузки
Fetch type отвечает на один вопрос: грузить relation сейчас или только когда код к ней обратится? Если у Order есть User, Hibernate может загрузить пользователя сразу вместе с заказом или подождать, пока код обратится к order.getUser(). Это решение влияет на performance, количество SQL, transaction boundaries и design API.
EAGER loading
FetchType.EAGER означает, что связанная entity должна быть загружена сразу. Сначала это кажется удобным, потому что поле доступно без отдельного явного query. В маленьких примерах это выглядит проще.
Недостаток в том, что eager loading может принести данные, которые текущему use case не нужны. Список заказов внезапно может загрузить пользователей, профили, роли или другие relationships. Это увеличивает memory usage и может создавать большие joins или много дополнительных queries. Если relationship eager, каждый query этой entity платит эту цену, если только более конкретный query не изменит план загрузки.
LAZY loading
FetchType.LAZY означает, что связь загружается только при первом обращении:
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
Когда order загружен, Hibernate может положить proxy вместо реального user object. Если код позже вызовет order.getUser().getEmail() внутри открытого persistence context, Hibernate отправит дополнительный SQL query и инициализирует связь.
Lazy loading обычно лучше как default для collections и многих relationships, потому что первый query остается сфокусированным. Service затем явно выбирает, что загрузить для конкретного экрана или endpoint, через join fetch, entity graphs или DTO queries.
| Сценарий endpoint | Что сделал бы EAGER | Что сделал бы LAZY | Лучшее решение |
|---|---|---|---|
| Список 50 orders только с id и status | Загрузил бы users, хотя response их не использует. | Сначала загрузил бы только orders. | Оставить relation lazy и вернуть order DTO. |
| Показать один order с customer email | Customer доступен сразу, но каждый order query платит эту цену. | Customer загрузится при обращении внутри transaction. | Lazy mapping плюс JOIN FETCH или DTO query для этого endpoint. |
| Сериализовать entity после выхода из service | Related data может быть загружена, но форма response плохо контролируется. | Lazy access может упасть с LazyInitializationException. | Преобразовать в DTO внутри service method. |
| Загрузить collection relation | Может создать очень большой query или несколько extra queries. | Не грузит collection до необходимости. | Предпочитать lazy collections и explicit fetching под use case. |
LazyInitializationException
LazyInitializationException возникает, когда код обращается к lazy relation после закрытия persistence context. Hibernate хочет загрузить данные, но у него уже нет активной session, связанной с базой.
Частая причина - возврат entities напрямую из service или controller, после чего JSON serialization позже трогает lazy fields. Исправление не в том, чтобы сделать все relationships eager. Лучше загрузить нужные данные внутри service, преобразовать entities в DTOs внутри transaction, использовать JOIN FETCH для конкретного query, применить @EntityGraph или создать DTO projection.
Что происходит в SQL
При eager loading один repository call может сразу создать join или дополнительные select statements. При lazy loading первый select загружает только owner table. Более позднее обращение к полю запускает еще один select для related entity. SQL logging - самый простой способ увидеть это во время обучения, но в production не стоит полагаться только на шумный SQL output.
Лучшие практики
Предпочитай lazy relationships по умолчанию, особенно для collections. Не возвращай JPA entities напрямую из public API. Проектируй service methods вокруг use cases и возвращай DTOs. Для каждого endpoint решай, какие fields и relations действительно нужны. Если query нужны related data, загружай их явно. Такой подход проще анализировать по performance, чем глобальные eager mappings.
Практика
Создай Order с lazy-связью на User. Загрузи список заказов и убедись, что users сначала не загружаются. Затем обратись к order.getUser().getEmail() внутри transaction и посмотри дополнительный SQL. Повтори обращение после закрытия transaction и получи LazyInitializationException. Исправь это через DTO query или JOIN FETCH.
Чек-лист понимания
- Могу объяснить, когда EAGER и LAZY загружают данные.
- Понимаю, почему EAGER не является универсальным решением.
- Могу объяснить
LazyInitializationException. - Понимаю, почему DTO безопаснее entity в API responses.
- Могу проверить fetch behavior через SQL logs.