Собеседование Backend-Java-разработчика: вопросы и где искать ответы. Часть 2

?v=1

Проблема 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.

Углубиться в проблему можно:


© Habrahabr.ru