Кейс: N+1 при обходе иерархии
Ситуация
Код собирал данные по иерархии сущностей. Для каждой записи требовалась связь с родителем. Функционально результат был правильным, но с ростом списка Hibernate выполнял всё больше SQL-запросов.
Что показала диагностика
Внутри цикла принудительно инициализировалась ленивая связь. Внешне это выглядело как обычный обход коллекции, но каждая итерация могла обращаться к базе:
1 запрос на основной список
+ N запросов на связи отдельных элементовПринудительная инициализация не устраняла N+1. Она лишь делала момент загрузки явным.
Мой вклад
Я изменил порядок работы:
- получил идентификаторы всех элементов, которые действительно нужны;
- одним запросом загрузил набор вместе с родительскими связями;
- построил нужные соответствия в памяти;
- убрал инициализацию ленивой связи из цикла.
Для загрузки использовал отдельный запрос с fetch join. Такой же принцип применил в нескольких местах, где вычислялись родительские и корневые элементы.
Результат
Количество запросов перестало расти линейно вместе с размером обрабатываемого списка. Обход иерархии стал обычной операцией над уже загруженными объектами.
Что я теперь проверяю
- нет ли обращения к ленивой связи внутри
for,stream.mapили mapper; - можно ли сначала собрать все ключи и загрузить данные пачкой;
- не создаёт ли сериализация DTO скрытые дополнительные запросы;
- измеряется ли реальное число SQL-запросов в тесте или логах.