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

Як включати зв'язки в API Resource без N+1 через whenLoaded і whenCounted?

Проблема: ресурс звертається до зв'язку, який не завантажено, - і на кожен елемент колекції виконується окремий запит.

// у ресурсі
'author' => new UserResource($this->author),   // N+1 для колекції з 50 постів - 51 запит

whenLoaded включає зв'язок у відповідь лише якщо його вже завантажено - і сам нічого не завантажує:

class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'author' => new UserResource($this->whenLoaded('author')),
            'tags' => TagResource::collection($this->whenLoaded('tags')),
            'comments_count' => $this->whenCounted('comments'),
            'average_rating' => $this->whenAggregated('reviews', 'rating', 'avg'),
        ];
    }
}

Якщо зв'язок не завантажено, ключ зникає з відповіді (а не стає null).

Контролер вирішує, що завантажити:

$posts = Post::query()
    ->with(['author', 'tags'])
    ->withCount('comments')
    ->withAvg('reviews', 'rating')
    ->paginate();

return PostResource::collection($posts);

Той самий ресурс у списку (мінімум зв'язків) і на сторінці деталей (більше зв'язків) - різна кількість полів без двох окремих класів.

Включення на вимогу клієнта (?include=author,tags) - дозволений перелік, а не довільні зв'язки:

$allowed = ['author', 'tags', 'comments'];
$includes = array_intersect(explode(',', $request->string('include')), $allowed);

$posts = Post::with($includes)->paginate();

Пакет spatie/laravel-query-builder робить це разом із фільтрами й сортуванням. Вбудований JsonApiResource у Laravel 13 підтримує include за специфікацією JSON:API.

Інші помічники ресурсів:

  • $this->when($condition, $value) - поле за умовою (права, контекст);
  • $this->mergeWhen($condition, [...]) - кілька полів разом;
  • $this->whenPivotLoaded('role_user', fn () => ...) - дані проміжної таблиці.

Як ловити N+1 в API:

  • Model::preventLazyLoading(! app()->isProduction()) - виняток при ледачому завантаженні в розробці й тестах;
  • Telescope, Debugbar - кількість запитів на ендпойнт;
  • тест, що перевіряє кількість запитів для колекції (DB::enableQueryLog() / expectsDatabaseQueryCount).

Пастка: whenLoaded приховує «відсутні» дані - клієнт може не помітити, що зв'язок перестав приходити, бо контролер забув with(). Тому у важливих ендпойнтах варто мати тести структури відповіді.

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

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