Какие способы решают проблему N+1 в Hibernate, кроме fetch type LAZY
Что проверяют
Понимаете ли вы, откуда берётся N+1 и что LAZY его не решает, а только откладывает до первого обращения к связи. Следом обычно спрашивают, как вы вообще заметите N+1 и что делать с пагинацией при загрузке коллекций.
Как отвечать
Начните с механики: один запрос достаёт N родителей, а потом на каждого уходит отдельный select за связью. По умолчанию в JPA @ManyToOne и @OneToOne загружаются EAGER, коллекции LAZY, и оба режима дают N+1 при JPQL-запросе.
join fetch:select p from Post p join fetch p.commentsгрузит всё одним запросом. В Hibernate 6 дубли родителей убираются автоматически,distinctдля этого больше не нужен;@EntityGraph: в Spring Data JPA@EntityGraph(attributePaths = "comments")на методе репозитория, в чистом JPA подсказкаjakarta.persistence.fetchgraph. Удобно, когда одна сущность нужна в разных сценариях с разным набором связей;- batch fetching:
@BatchSize(size = 50)или глобальноhibernate.default_batch_fetch_size. Вместо N запросов уходит примерно N/50 сINпо идентификаторам; @Fetch(FetchMode.SUBSELECT): коллекции всех загруженных родителей подтягиваются одним подзапросом;- DTO-проекции:
select new ...Dto(...)или интерфейсные проекции Spring Data. Нет сущностей, нет ленивых связей, берутся только нужные колонки.
Как заметить: hibernate.generate_statistics, логирование SQL и тесты, которые считают число запросов через datasource-proxy.
Где спотыкаются
Ставят EAGER, думая, что это одна загрузка. Для JPQL-запроса Hibernate сначала достаёт родителей, а затем догружает eager-связи отдельными запросами, получается тот же N+1.
Делают join fetch коллекции вместе с пагинацией. Hibernate выдаёт предупреждение, выгружает всё и режет страницу в памяти. Правильно в два шага: страница идентификаторов, затем fetch по ним. Два List в одном fetch дают MultipleBagFetchException.
Лечат N+1 кэшем второго уровня или не замечают его из-за spring.jpa.open-in-view, который в Spring Boot включён по умолчанию. Ленивые связи тогда тихо догружаются при сериализации в JSON, и запросы множатся уже в контроллере.