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

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

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

Загальну механіку N+1 і with() ми вже розбирали в статтях «Найдешевший запит - той, якого немає» і «Зупинка проблеми N+1 запитів», тож тут лише зворотний напрямок. Відправною точкою стали дві поради Джоела Клермонта з Mastering Laravel - про chaperone() і про автоматичне завантаження; назви методів і поведінку звірили з документацією Laravel 13.

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

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

  $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 під час завантаження дочірніх моделей одразу проставити кожній батьківську. Його можна закріпити на зв'язку:

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

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

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

  $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. Сам метод з'явився ще в Laravel 11.22 (PR #51582).

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

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

  use Illuminate\Database\Eloquent\Model;

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

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

  $posts = Post::all();

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

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

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

  $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 змерджив сам Тейлор Отвелл. Тобто для Laravel 13 документація функцію експериментальною вже не вважає, а на 12.x попередження лишається чинним.

Власні колекції. Якщо модель повертає свою колекцію через перевизначений newCollection(), автозавантаження саме собою на неї не поширюється. Документація до власних колекцій показує, що треба додати перевірку Model::isAutomaticallyEagerLoadingRelationships() і викликати withRelationshipAutoloading() на новій колекції.

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

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

Читати в документації

Коментарі

Увійдіть, щоб залишити коментар

Будьте першим, хто залишить коментар!

Читайте також

Новини 02 листопада 2026

HTTP-метод QUERY і пауза polling в Inertia.js v3.10

Inertia.js v3.10.0 вміє робити візити методом QUERY через router.query(), Link і Form, а inertia-laravel v3.6.0 перетворює редіректи після них на 303. У v3.9 з'явилася опція background для пауз polling у фонових вкладках, а v3.9.1 закрила дві тихі проблеми безпеки.

Новини 31 жовтня 2026

Composer 2.11: cooldown для нових версій пакетів і виправлення безпеки

Composer 2.11 додав політику cooldown, яка не дає встановити версію пакета, опубліковану менше ніж заданий час тому. У випуску також низка виправлень безпеки, прибраний перехід із dist на source і зміни, що можуть зачепити CI. Розбираємо, як налаштувати cooldown і що перевірити після оновлення.

13

Вакансії за темою

Senior PHP Developer

Senior PHP розробник для логістичної платформи: бекенд-сервіси на PHP 8.3+ та Laravel 11 і веб-панель на Filament 3 та Livewire 3. Завдання: REST API, авторизація, інтеграції через API і вебхуки, оптимізація PostgreSQL, черги та кешування. Вимоги: комерційний досвід Laravel, знання PostgreSQL, Redis, WebSockets, CI/CD та діагностики продакшн-проблем. Віддалено, також розглядають сильних Middle+.

Join UP! Нова
Сьогодні

Backend Developer (PHP, Laravel)

Middle Backend розробник на повний цикл веб-системи: серверна логіка, бізнес-процеси, проєктування та розробка API, оптимізація SQL-запитів і транзакцій у PostgreSQL, кешування, черги та фонова обробка. Стек: PHP 8.3+, Laravel 11+, Docker, CI/CD. Потрібен досвід від 5 років у backend, з них щонайменше 3 роки з Laravel, уміння писати автотести та проводити code review.

ConveyThis Нова
Сьогодні

Middle PHP Developer

Розробка, підтримка та оптимізація веб-застосунків на PHP (Laravel) з використанням SQL/NoSQL, JavaScript і Bootstrap. Завдання включають забезпечення якості коду, безпеки та масштабованості, співпрацю з дизайнерами та продакт-менеджерами, усунення вузьких місць продуктивності. Вимоги: 3+ роки досвіду з PHP і Laravel, знання баз даних, JavaScript, Git, ООП, англійська від Intermediate. Віддалена робота з обов'язковим перекриттям з часом Нью-Йорка.

Пакети за темою

Kalimera

welldigit/kalimera

Interactive installer that scaffolds Laravel applications the Well Digit way.

v1.0.0 16

Bagisto

bagisto/bagisto

Bagisto — це платформа для електронної комерції, побудована на Laravel. Вона надає готове рішення для створення та управління інтернет-магазинами з підтримкою каталогу товарів, замовлень, платежів та клієнтів.

28,293 v2.5.0 13 27