Opublikowano 3 min. czytania

Używając tak wygodnych narzędzi jak Eloquent ORM, łatwo jest zapomnieć o problemie N+1. Ignorowanie go może doprowadzić do nieprzewidzianych problemów z wydajnością serwera. W tym artykule wyjaśniam czym ten problem jest, jak go rozwiązać oraz jak szybko wykryć go na etapie developmentu.
fot. Johannes Plenio (pexels.com)

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:

Zapytania SQL wyświetlone w Laravel Telescope

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:

Zapytania SQL wyświetlone w Laravel Telescope

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.

Laravel N+1 query detector

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.

Wyjątek dotyczący lazy loading'u