Как Hibernate строит запрос для загрузки сущности с дочерними объектами и как сделать join?
Что проверяют
Понимаете ли вы, какой SQL реально уходит в базу при обращении к связанной коллекции, и знаете ли проблему N+1. Следом спрашивают, почему fetch сразу двух коллекций падает или раздувает результат и как совместить его с пагинацией.
Как отвечать
Запрос определяют маппинг и тип загрузки:
- FetchType по умолчанию: в JPA
@OneToManyи@ManyToManyленивые,@ManyToOneи@OneToOneжадные. Приfind()Hibernate грузит родителя, а коллекция становится обёрткойPersistentBagилиPersistentSet, которая сделает отдельныйSELECTпри первом обращении; - N+1: загрузили 100 заказов и в цикле обратились к позициям, получили 1 + 100 запросов. Вне открытой сессии вместо этого прилетит
LazyInitializationException; - JOIN FETCH:
select o from Order o join fetch o.items where ...подтягивает коллекцию одним запросом. В Hibernate 6 повторы родителя, которые даёт join, убираются автоматически,distinctдля этого больше не нужен; - обычный join:
join o.items i where i.price > 100только фильтрует, коллекцию он не инициализирует; - Criteria API:
root.fetch("items", JoinType.LEFT)для загрузки,root.join(...)для условий; - @EntityGraph: в Spring Data JPA
@EntityGraph(attributePaths = "items")на методе репозитория даёт тот же fetch без переписывания запроса; - @BatchSize и
FetchMode.SUBSELECT: не join, но превращают N+1 в пару запросов сIN (...).
EAGER на коллекции решением не считается: он срабатывает везде, включая места, где связь не нужна, и сам порождает N+1 в JPQL-запросах.
Где спотыкаются
Делают join fetch сразу по двум List и получают MultipleBagFetchException, а с Set получают декартово произведение строк. Выход: один fetch на запрос, остальное через @BatchSize или второй запрос.
Сочетают join fetch коллекции с setMaxResults. Hibernate пишет предупреждение, выгружает всё и режет страницу в памяти. Правильно сначала выбрать идентификаторы страницы, потом загрузить их с fetch.
Лечат LazyInitializationException через open-in-view или enable_lazy_load_no_trans. Ошибка пропадает, а N+1 остаётся, только теперь незаметно.