---
title: "N+1 у зворотний бік: chaperone() і автоматичне жадібне завантаження в Eloquent"
url: https://laravelukraine.com/blog/n1-u-zvorotnii-bik-chaperone-i-avtomaticne-zadibne-zavantazennia-v-eloquent
date: 2026-10-31
---

# N+1 у зворотний бік: chaperone() і автоматичне жадібне завантаження в Eloquent

Жадібне завантаження (Eager Loading) через `with()` прибирає проблему N+1 лише в одному напрямку - від батьківської моделі до дочірніх. Варто в циклі по коментарях звернутися назад до поста, і Eloquent знову піде в базу за кожним коментарем, хоча пост уже лежить у пам'яті. На цей випадок у фреймворку є два інструменти, про які знають небагато: `chaperone()` і автоматичне жадібне завантаження зв'язків.

Загальну механіку N+1 і `with()` ми вже розбирали в статтях [«Найдешевший запит - той, якого немає»](https://laravelukraine.com/blog/naidesevsii-zapit-toi-iakogo-nemaje-eager-loading-i-licilniki) і [«Зупинка проблеми N+1 запитів»](https://laravelukraine.com/blog/zupinka-problemi-n1-zapitiv), тож тут лише зворотний напрямок. Відправною точкою стали дві поради Джоела Клермонта з Mastering Laravel - [про `chaperone()`](https://masteringlaravel.io/daily/2026-10-22-why-does-eloquent-query-for-a-parent-it-already-loaded) і [про автоматичне завантаження](https://masteringlaravel.io/daily/2026-10-27-let-eloquent-eager-load-relationships-for-you); назви методів і поведінку звірили з [документацією Laravel 13](https://laravel.com/docs/13.x/eloquent-relationships).

### Звідки береться 14 запитів

Візьмімо звичайну пару: `Post` має багато `Comment` (`hasMany`), а `Comment` належить `Post` (`belongsTo`). У базі 3 пости і 12 коментарів. Виводимо коментарі разом із заголовком їхнього поста:

```php
  $posts = Post::with('comments')->get();

  foreach ($posts as $post) {
      foreach ($post->comments as $comment) {
          echo $comment->post->title;
      }
  }
```

Тут 14 запитів: два на `with()` (пости і всі їхні коментарі) і ще по одному на кожен із 12 коментарів. Причина в тому, що коментар не знає, через який пост його завантажили. Для нього `post` - звичайний ще не завантажений зв'язок, тож `$comment->post` робить ліниве завантаження (Lazy Loading). Документація прямо називає це N+1: Eloquent не гідратує батьківський `Post` у кожен дочірній `Comment` автоматично.

### chaperone(): батько без жодного запиту

Метод `chaperone()` просить Eloquent під час завантаження дочірніх моделей одразу проставити кожній батьківську. Його можна закріпити на зв'язку:

```php
  public function comments(): HasMany
  {
      return $this->hasMany(Comment::class)->chaperone();
  }
```

Той самий цикл тепер робить 2 запити, скільки б коментарів не було. Важливо, що `$comment->post` - не копія, а той самий екземпляр моделі, що й `$post` у зовнішньому циклі: зміна одного видна в іншому.

Якщо не хочеться вмикати це для зв'язку назавжди, `chaperone()` можна викликати точково, під час жадібного завантаження:

```php
  $posts = Post::with([
      'comments' => fn ($comments) => $comments->chaperone(),
  ])->get();
```

Назву зворотного зв'язку Eloquent вгадує з зовнішнього ключа або назви батьківської моделі. Якщо в `Comment` зв'язок названо інакше, наприклад `article()`, передайте назву явно: `chaperone('article')`.

`chaperone()` працює для `hasMany` і `morphMany`, а з Laravel 13.31 - і для власних pivot-моделей у `belongsToMany`, про що ми писали в [огляді Laravel 13.31](https://laravelukraine.com/blog/laravel-1331-metod-totalsize-dlia-cerg-podiia-jobinterrupted-ta-pokrashhennia-belongstomany). Сам метод з'явився ще в Laravel 11.22 ([PR #51582](https://github.com/laravel/framework/pull/51582)).

### Автоматичне жадібне завантаження

Другий інструмент ширший. З Laravel 12.8 ([PR #53655](https://github.com/laravel/framework/pull/53655)) можна попросити фреймворк самому довантажувати зв'язки, до яких ви звертаєтеся. Вмикається глобально, у `boot()` вашого `AppServiceProvider`:

```php
  use Illuminate\Database\Eloquent\Model;

  public function boot(): void
  {
      Model::automaticallyEagerLoadRelationships();
  }
```

Тепер той самий цикл можна писати взагалі без `with()`:

```php
  $posts = Post::all();

  foreach ($posts as $post) {
      foreach ($post->comments as $comment) {
          echo $comment->post->title;
      }
  }
```

Коли ви вперше звертаєтеся до `$post->comments`, Laravel одним запитом завантажує коментарі для всіх постів колекції. Коли вперше звертаєтеся до `$comment->post` - так само завантажує пости для всіх коментарів. Разом 3 запити замість 16, які цей код зробив би без `with()` і без автозавантаження.

Якщо глобально вмикати не хочеться, автозавантаження можна ввімкнути для однієї колекції:

```php
  $posts = Post::all()->withRelationshipAutoloading();
```

### Чим вони відрізняються і як працюють разом

Третій запит у прикладі вище - якраз та різниця. Автоматичне завантаження все одно йде в базу по пости, хоча вони вже завантажені, і повертає окремі екземпляри. Змініть щось у `$comment->post` - і `$post` у зовнішньому циклі цього не побачить. `chaperone()` цей запит пропускає і віддає той самий об'єкт.

Інструменти не конкурують. Якщо ввімкнути автоматичне завантаження і додати `chaperone()` на зв'язок `comments()`, цикл знову зводиться до 2 запитів. Підсумок для нашого прикладу:

- `with('comments')` - 14 запитів;
- `with('comments')` і `chaperone()` - 2;
- автоматичне жадібне завантаження без `with()` - 3;
- автоматичне завантаження і `chaperone()` - 2.

### Застереження

**`chaperone()` змінює вміст моделей.** Кожен коментар тепер має завантажений зв'язок `post`, тож `toArray()` і JSON-серіалізація вкладають батьківську модель у кожну дочірню. Якщо ви віддаєте моделі з API напряму, без API Resources, дані відповіді зміняться - саме тому `chaperone()` не ввімкнено за замовчуванням. Вмикайте його свідомо і перевірте, що повертають ваші ендпоінти.

**Статус «beta» в автозавантаження вже зняли, але лише в документації 13.x.** Порада Mastering Laravel застерігає, що документація позначає функцію як beta, поведінка якої може змінитися навіть у патч-релізах. Такий блок досі стоїть у документації 12.x, а з документації 13.x його прибрали в серпні: [PR #11317](https://github.com/laravel/docs/pull/11317) змерджив сам Тейлор Отвелл. Тобто для Laravel 13 документація функцію експериментальною вже не вважає, а на 12.x попередження лишається чинним.

**Власні колекції.** Якщо модель повертає свою колекцію через перевизначений `newCollection()`, автозавантаження саме собою на неї не поширюється. Документація до [власних колекцій](https://laravel.com/docs/13.x/eloquent-collections) показує, що треба додати перевірку `Model::isAutomaticallyEagerLoadingRelationships()` і викликати `withRelationshipAutoloading()` на новій колекції.

### Що з цього вмикати

- **`chaperone()`** варто додати на `hasMany`-зв'язки, де ви регулярно ходите від дитини до батька: коментарі й пости, рядки замовлення й замовлення. Запиту він не додає взагалі, а єдина ціна - вкладена модель у серіалізації. Де моделі йдуть в API без ресурсів, обирайте точковий виклик у `with([...])`.
- **Автоматичне жадібне завантаження** зручне як страховка на випадок, коли ви не знаєте наперед, які зв'язки знадобляться, - наприклад, у шаблонах із розгалуженою логікою. Але воно не скасовує явних `with()`: явне завантаження видно в коді, а автоматичне ховає запити від того, хто читає контролер.
- **Перед глобальним увімкненням** на великому проєкті почніть з `withRelationshipAutoloading()` на кількох важких сторінках і подивіться на лічильник запитів, наприклад у Laravel Debugbar чи Telescope. Якщо ви ще на Laravel 12, зважайте на статус beta.
- **`Model::preventLazyLoading()`** у середовищі розробки лишається найпростішим способом помітити, де взагалі ховається N+1, - а вже потім вирішувати, чим його лагодити.
