Кейс: сетевой N+1
Ситуация
Процесс формировал сводный результат по списку объектов. Внутри обхода он повторно получал связанные данные: часть — через удалённые вызовы платформы, часть — из локального хранилища.
для каждого элемента:
получить связанные данные
получить сведения о пользователе
добавить строку результатаЧисло обращений росло вместе с размером входного списка. Это оказался тот же N+1, только не внутри ORM-загрузки одной сущности.
Диагностика
Я выписал обращения внутри цикла и разделил их на две группы:
- данные, которые уже можно загрузить набором из локального repository/cache;
- данные, для которых остаётся только одиночный удалённый вызов.
Отдельно нашёл повторяющиеся идентификаторы: один и тот же удалённый результат не требовалось получать заново для каждой строки.
Мой вклад
Я заранее собрал идентификаторы, массово загрузил доступные локально данные и построил карту для обогащения результата:
Map<Long, Details> detailsByOwner = detailsRepository
.findByOwnerIdIn(ownerIds)
.stream()
.collect(toMap(Details::ownerId, identity()));Для оставшегося удалённого API я сначала выделил уникальные идентификаторы, один раз получил результат для каждого из них и повторно использовал его при сборке строк. Batch-контракта у этого вызова не было.
Результат
Повторные обращения к данным платформы исчезли из цикла, а оставшиеся удалённые вызовы перестали дублироваться для одинаковых идентификаторов. Бизнес-результат не изменился.
Технический вывод
Если внутри цикла есть обращение к repository, cache или удалённому API, это повод искать N+1. Возможные решения: предварительная массовая загрузка, дедупликация ключей, кэш в рамках операции, batch endpoint или изменение контракта.
Массовый запрос тоже имеет ограничения: размер набора, timeout, частичные ошибки и память.