Как реализовать ленивую и жадную загрузку связанных сущностей в Hibernate с помощью FetchType
Что проверяют
Знание значений FetchType по умолчанию, понимание, где возникает LazyInitializationException и N+1, и умение назвать конкретные способы управлять загрузкой на уровне запроса, а не маппинга.
Как отвечать
Начать нужно с дефолтов JPA, это половина ответа:
@OneToManyи@ManyToManyпо умолчанию LAZY;@ManyToOneи@OneToOneпо умолчанию EAGER.
Именно второй пункт создаёт большинство проблем: ссылка на родителя грузится всегда, тянет за собой его собственные жадные связи, и простой запрос за одной сущностью разворачивается в цепочку джойнов. Поэтому в маппинге почти всегда явно пишут fetch = FetchType.LAZY, а жадность задают в конкретном запросе.
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
@OneToMany(mappedBy = "customer", fetch = FetchType.LAZY)
private Set<Order> orders;LAZY: вместо коллекции подставляется прокси, запрос уходит при первом обращении. Условие: открытая сессия. Обращение к незагруженной связи вне транзакции даёт LazyInitializationException, классически в контроллере или в сериализаторе JSON. В Spring Boot это маскирует включённый по умолчанию open-session-in-view, но правильнее его выключить и грузить нужное явно.
EAGER: грузится всегда, и это нельзя отменить в конкретном запросе. Обратного превращения в ленивую загрузку на уровне запроса нет, поэтому EAGER в маппинге считается плохой практикой.
N+1 возникает при обоих режимах: при LAZY, когда коллекцию обходят в цикле по родителям, и при EAGER, когда Hibernate делает отдельный select на каждую сущность. Инструменты против него:
JOIN FETCHв JPQL:select c from Customer c join fetch c.orders where c.active = true;@EntityGraphна методе репозитория Spring Data, декларативно и без ручного JPQL;@BatchSize(size = 50): связи догружаются пачками, N запросов превращаются в N/50;- DTO-проекция, когда нужна пара полей, а не целые сущности.
Ограничения, которые стоит проговорить: JOIN FETCH для двух коллекций типа List без @OrderColumn даёт MultipleBagFetchException (лечится Set или двумя запросами), а JOIN FETCH вместе с пагинацией заставляет Hibernate вытянуть весь набор в память и резать там, о чём в лог пишется предупреждение HHH000104.
Отдельный нюанс: @OneToOne с LAZY на стороне mappedBy не работает, Hibernate обязан узнать, существует ли связанная строка, и прокси поставить не может. Помогает optional = false на владеющей стороне или bytecode enhancement.
Где спотыкаются
Лечат LazyInitializationException переводом связей в EAGER. Исключение уходит, а вместо него приходят лишние джойны на каждый запрос и тот же N+1, только теперь без возможности его отключить.