Защита от ленивой загрузки не перехватывает все 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.