Дві протилежні проблеми 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: умовні зв'язки в ресурсах