Czym jest problem N+1?
Załóżmy, że mamy zapytanie, które wyciąga wszystkie przepisy z bazy:
$recipes = Recipe::all();
Model Recipe
posiada relację belongsTo
do modelu Category
:
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
W widoku blade wyświetlamy przepisy z ich kategorią:
@foreach ($recipes as $recipe)
<article>
<h2>{{ $recipe->name }}</h2>
<p>{{ $recipe->category->name }}</p>
</article>
@endforeach
Użyjmy Laravel Telescope i zobaczmy jakie zapytania są wykonywane podczas wyświetlania powyższej listy:
Co się stało? Wyświetlanie 10 przepisów, generuje 11 zapytań! Dodatkowe zapytanie jest wykonywane dla każdego przepisu, aby pobrać nazwę kategorii! Wyobraź sobie co się stanie, kiedy rekordów i relacji będzie więcej. To marnowanie zasobów.
Rozwiązanie
Po prostu unikaj lazy loading'u i korzystaj z eager loading. W Eloquencie osiągniesz to za pomocą metody with
. Nawiązując do powyższego przykładu, nasze zapytanie mogłoby wyglądać tak:
$recipes = Recipe::with('categories')->all();
lub jeśli chcesz załadować więcej relacji na raz:
$recipes = Recipe::with(['categories', 'tags', 'author'])->all();
To rozwiązanie zmniejsza liczbę zapytań z 11 do 2:
Użyteczne narzędzia
Jak widzisz, rozwiązanie było proste. Najtrudniejsze jest pilnowanie, czy w projekcie nie tworzymy niepotrzebnych zapytań. To może się zdarzyć, kiedy kilka osób pracuje nad tym samym projektem, zwłaszcza jeśli front-end developer przygotowuje widoki, a ktoś inny odpowiedzialny jest za backend.
Dostępne są paczki takie jak Laravel Telescope (której użyłem wyżej) lub Laravel Debugbar, za pomocą których możesz ręcznie przejrzeć zapytania, ale są również narzędzia i techniki, które natychmiastowo powiadomią Cię o wystąpieniu problemu N+1.
Laravel N+1 Query Detector
Doskonała i prosta paczka, która monitoruje zapytania w czasie rzeczywistym podczas developmentu aplikacji. Po prostu zainstaluj ją composerem i otrzymasz alert, kiedy eager loading powinien zostać użyty.
Możesz ją pobrać z GitHub'a: beyondcode/laravel-query-detector
Wyłącz lazy loading globalnie
Laravel 8 daje Ci fajną możliwość wyłączenia lazy loading'u w całej aplikacji. Dodaj poniższą linię do metody boot
w pliku app\Providers\AppServiceProvider.php
:
<?php
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
Model::preventLazyLoading( ! app()->isProduction());
}
}
W ten sposób za każdym razem, gdy lazy loading zostanie użyty, będzie wyrzucony wyjątek. Stanie się tak tylko w nie-produkcyjnym środowisku, dlatego nie musisz się martwić wystąpieniem błędu na produkcji, kiedy coś przeoczysz.