Защита от ленивой загрузки не перехватывает все N+1 запросы

Источник: «Lazy loading protection does not catch all N+1 queries»
Я большой поклонник нового "strict mode" для моделей Laravel. Одна из вещей, которую он помогает отловить, — это ужасный запрос "N+1", вызывающий проблемы с производительностью в продакшене.

Приятно, когда тест не проходит локально, предупреждая вас об одной из таких проблем до того, как она попадёт в продакшн. Но недавно я столкнулся с проблемой, когда Sentry (наш инструмент мониторинга ошибок и производительности) сообщил о "N+1", которую не смог поймать "strict mode" Eloquent.

Реальный код нашего приложения довольно сложен, чтобы показать его в коротком примере, но рассмотрим этот фрагмент кода:

// намеренно плохой пример кода — НЕ КОПИРОВАТЬ
$users = User::all();
foreach ($users as $user) {
$user->load('payments');
}

Этот ужасный пример кода существует только для того, чтобы продемонстрировать проблему. Обратите внимание, что здесь мы определённо вызываем "N+1" запрос, даже несмотря на использование функции нетерпеливой загрузки Laravel.

Если в нашей базе данных 100 пользователей, то этот код приведёт к 101 запросу. Если мы просто вынесем вызов load() за пределы цикла, то сможем сократить это количество до 2 запросов.

Что меня удивило, так это то, что Laravel не сообщает об этом даже при включённой строгости Eloquent. Причина в том, что Laravel отслеживает вызовы только через методы отношений, а не анализирует реальные генерируемые запросы и ищет дублирование.

Поэтому этот код не выдержит строгости Eloquent:

// ещё один намеренно плохой пример кода — НЕ КОПИРОВАТЬ
$users = User::all();
foreach ($users as $user) {
$userPaymentCount = $user->payments->count();
}

Поскольку мы используем отношения payments, Laravel сообщит об этом как о запросе "N+1". К счастью, Sentry анализирует фактические генерируемые запросы и предупреждает нас о проблеме. Об этом нюансе стоит знать, особенно если у вас нет такого инструмента, как Sentry.

Дополнительные материалы

Предыдущая Статья

Запуск Laravel Pint как части CI-конвейера с помощью Github Actions

Следующая Статья

Несколько способов упростить CSS в 2023 году