Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Як уникнути N+1 запитів і надлишкових даних у REST API на Laravel?

Дві протилежні проблеми REST:

  • надлишкові дані (over-fetching) - відповідь містить усе, хоча клієнту потрібна дрібка;
  • недостатні дані (under-fetching) - для одного екрана потрібно кілька послідовних запитів.

На сервері обидві часто перетворюються на N+1: ресурс звертається до зв'язку для кожного елемента списку.

// контролер
return PostResource::collection(Post::paginate(20));

// ресурс
public function toArray($request): array
{
    return [
        'id' => $this->id,
        'title' => $this->title,
        'author' => new UserResource($this->author),   // окремий запит на кожен пост
    ];
}

20 постів - 21 запит до бази.

Рішення 1 - жадібне завантаження + умовні зв'язки:

// контролер
return PostResource::collection(Post::with('author')->paginate(20));

// ресурс
'author' => UserResource::make($this->whenLoaded('author')),
'comments_count' => $this->whenCounted('comments'),

whenLoaded додає зв'язок у відповідь лише якщо його завантажили. Ресурс більше не робить запитів сам, а контролер явно вирішує, що завантажити.

Рішення 2 - include на запит клієнта: GET /api/posts?include=author,tags. Контролер завантажує лише дозволені зв'язки з цього списку (spatie/laravel-query-builder чи вбудовані JSON:API-ресурси Laravel, які серіалізують зв'язок лише коли клієнт його запросив).

Рішення 3 - вибір полів: fields[posts]=id,title - мобільний список не тягне тіло статті.

Захист від N+1 у розробці:

// AppServiceProvider::boot()
Model::preventLazyLoading(! app()->isProduction());

Ліниве завантаження зв'язку кидає виняток у розробці й тестах - N+1 видно одразу. Також Model::automaticallyEagerLoadRelationships() (Laravel 12+) підвантажує зв'язки для всієї колекції автоматично - зручно, але не замінює свідомого with().

Агрегати замість колекцій: withCount('comments'), withSum, withExists - кількість одним запитом, а не завантаження всіх коментарів заради count().

Коли REST не вистачає: якщо різні клієнти постійно потребують дуже різних наборів даних, а include/fields розростаються, - це аргумент за GraphQL або окремі ендпойнти під конкретний клієнт (Backend for Frontend).

Перевірка: Debugbar, Telescope чи тест, що рахує запити (DB::enableQueryLog() + expect(count(DB::getQueryLog()))->toBeLessThan(5)).

Докладніше в документації: Laravel: умовні зв'язки в ресурсах

Перевір себе

20 випадкових питань за спробу, після завершення - розбір кожної помилки

Схожі питання