Как исправить проблему N+1 в Hibernate на примере выборки автора и его книг
Что проверяют
Понимаете ли вы, откуда берутся N+1 запросов, и знаете ли больше одного способа их убрать вместе с ценой каждого. Следом почти всегда спрашивают про пагинацию вместе с fetch join и про то, как вы вообще заметили проблему.
Как отвечать
Механика: select a from Author a возвращает N авторов, а вызов author.getBooks() у каждого инициализирует ленивую коллекцию отдельным запросом. Итого 1 + N. Способы исправить:
left join fetch:select a from Author a left join fetch a.books. Именноleft, иначе авторы без книг пропадут. В Hibernate 6 дубли родителей убираются сами,distinctдля этого не нужен;@EntityGraph: в Spring Data@EntityGraph(attributePaths = "books")на методе репозитория, в JPA хинтjakarta.persistence.fetchgraph. Тот же join, но запрос не переписывается под каждый случай;- пакетная загрузка:
@BatchSize(size = 50)на коллекции илиhibernate.default_batch_fetch_size. Книги грузятся запросами сINпо пачке авторов, выходит 1 + N/50, и это дружит с пагинацией; @Fetch(FetchMode.SUBSELECT): одним подзапросом подгружает книги всех авторов из исходной выборки;- DTO-проекция: если сущности не нужны для изменения, выберите сразу нужные поля.
Как находить: лог SQL, hibernate.generate_statistics, а в тестах проверка числа запросов через обёртку над DataSource вроде datasource-proxy.
Где спотыкаются
Ставят FetchType.EAGER. Для JPQL-запроса это не join: Hibernate выполнит те же 1 + N, только теперь всегда. И помните, что у @ManyToOne по умолчанию именно EAGER, так что выборка книг даёт N+1 уже по авторам.
Делают join fetch коллекции вместе с setMaxResults. Ограничить строки в SQL Hibernate не может, выгружает всё и режет страницу в памяти с предупреждением в логе. Выход: сначала выбрать id страницы и догрузить по ним, либо @BatchSize.
Тянут fetch join двух коллекций типа List и получают MultipleBagFetchException, а с Set декартово произведение строк. Вторую коллекцию лучше загрузить отдельным запросом.