Собеседование Backend-Java-разработчика: вопросы и где искать ответы. Часть 2
Проблема N+1 может возникнуть не только при использовании Hibernate, но и других библиотек и фреймворков для доступа к данным.
В общем случае говорят о проблеме N+1 запроса, когда фреймворк выполняет N дополнительных запросов выборки данных, когда можно было обойтись всего одним. Соответственно от размера N зависит влияние проблемы на время ответа нашего приложения. Эту ситуацию нельзя обнаружить с помощью slow query log
, ибо сами по себе запросы могут выполняться быстро, но их количество окажется большим или даже огромным.
На такое можно нарваться даже при использовании plain sql (jdbc, JOOQ), когда у нас одна сущность (и соответственно таблица) связана с другой. И вот мы подгрузили одним запросом просто список из первых, а потом пошли и в цикле для каждой подгрузили связанную по одному запросу. «Да как вы это допустили?!». Да просто по запарке кто-то в цикле начал вызывать метод, у которого в глубине где-то делается запрос и привет. Как исправить? Использовать JOIN
со связанной таблицей при чтении списка. Тогда понадобиться лишь один запрос.
Теперь к Hibernate. Если на странице документации поискать «N+1», то можно обнаружить несколько упоминаний данной проблемы. Тут опишу самые явные и распространённые.
Например, возьмём стратегию выборки FetchType.EAGER
. Она склонна к порождению N+1. А в отношении @ManyToOne
по умолчанию используется именно она. Забыли в своём JPQL запросе заиспользовать JOIN FETCH
и привет. А если нам и не нужны были связанные сущности, то тогда стоит задать стратегию FetchType.LAZY
.
Если уж упомянули FetchType.LAZY
, то сразу стоит сказать, что одно её наличие не гарантирует отсутствие проблемы N+1. При выборке списка сущностей, связанные автоматически не подгрузились. А мы потом пошли в цикле по загруженному списку и стали обращаться к полям связанной сущности — и снова здравствуйте. Всё тот же JOIN FETCH
нас спасёт и в этой ситуации.
Но JOIN FETCH
во многих случаях нас может привести к декартовому произведению, и тогда будет совсем bonjour. Для отношения @OneToMany
это можно решить с помощью FetchMode.SUBSELECT
— будет 2 запроса, но во втором запросе на получение списка связанных сущностей в условии выборки будет подзапрос на получение идентификаторов родительских сущностей. Т.е. запрос практически повторяется и он может быть тяжеловесным.
Есть вариант лучше — вычитывать связанные сущности пачками. Мы можем добавить аннотацию @BatchSize
и указать размер подгружаемой пачки записей в одном запросе.
Ещё варианты:
Чтобы обнаружить проблему N+1, нужно писать тесты с использованием библиотеки db-util от Vlad Mihalcea. Подробнее можно прочитать у него же в блоге.
А вот JOOQ умеет обнаруживать N+1 автоматически, послушать об этом можно в 17-м эпизоде (01:16:36) подкаста Паша+Слава. Подробнее в документации JOOQ.
Углубиться в проблему можно: