У реальному проєкті майже немає “самотніх” таблиць. Новина має автора, розділ, ресурс, теги, статуси, лог активності. Клієнт має рахунки, рахунок має позиції, позиція посилається на сервіс. У Laravel всі ці зв’язки описуються через relations у моделях, і це критично: правильно описані зв’язки роблять код коротким, читабельним і швидким. Неправильно — породжують N+1, повільні сторінки і хаос у запитах.
Почнемо з базового: зв’язки — це спосіб сказати Eloquent, як таблиці пов’язані через ключі. У базі це виглядає як foreign key, а в коді — як метод у моделі. Коли ти один раз описав зв’язок, ти можеш зручно діставати пов’язані дані, фільтрувати по них, рахувати кількість і будувати адмінські списки без ручних JOIN.
belongsTo — зв’язок “цей запис належить іншому”. Ти використовуєш belongsTo там, де в твоїй таблиці лежить foreign key. Наприклад, новина “належить” користувачу, бо у news є user_id. Новина “належить” ресурсу, бо є resource_id. Лог активності “належить” новині і користувачу, бо має news_id і user_id. Логіка проста: якщо в таблиці є поле *_id, майже завжди це belongsTo.
hasMany — зв’язок “у цього запису багато дочірніх”. Це протилежна сторона belongsTo. Якщо користувач має багато новин, то у моделі User буде hasMany на News. Якщо новина має багато записів активності, то у моделі News буде hasMany на Activity. Це важливо для сторінок “показати все пов’язане”: наприклад, вивести всі рахунки клієнта або всі зміни по статті.
belongsToMany — зв’язок “багато-до-багатьох” через pivot-таблицю. Класичний приклад: новини і теги. Одна новина має багато тегів, і один тег належить багатьом новинам. У базі це реалізується проміжною таблицею, наприклад news_tag, де лежать news_id і tag_id. В Eloquent це описується belongsToMany з обох сторін, і ти отримуєш зручне API для приєднання/від’єднання тегів.
Коли зв’язки описані, новачок часто робить наступну помилку: він використовує їх у списку і не думає, скільки запитів виконується. Тут з’являється проблема N+1. N+1 означає: ти витягнув список з N записів одним запитом, а потім для кожного запису окремо витягуєш пов’язане (наприклад автора). У підсумку замість 2–3 запитів ти отримуєш 51, 201 або 1001. На маленькій базі це непомітно, на реальній — вбиває швидкість.
Як виглядає N+1 в житті. У тебе сторінка “останні 50 новин”, і в таблиці ти показуєш ім’я автора. Якщо ти зробив NationalNews::latest()->paginate(50) і в Blade пишеш $news->user->name, Eloquent почне підвантажувати користувача для кожної новини окремо. Це і є N+1. Лікування — eager loading.
Eager loading — це “підвантажити пов’язані дані наперед одним додатковим запитом”. Ти кажеш: “витягни мені новини і одразу користувачів для цих новин”. Laravel зробить два запити: перший по новинах, другий по користувачах (з where in по id). В результаті ти не робиш 50 окремих запитів. Це база продуктивності для будь-якої адмінки.
Важливо розуміти, що eager loading не означає “підвантажуй все підряд”. Підвантажувати треба лише те, що реально потрібно на сторінці. Якщо у списку ти показуєш автора і ресурс — підвантажуєш user і resource. Якщо ще показуєш теги — підвантажуєш tags. Але якщо теги не показуєш — не тягни їх, бо це зайві запити й зайва пам’ять.
Для eager loading використовують with(). Ключовий стандарт такий: якщо ти у Blade звертаєшся до $model->relation, то в контролері цей relation має бути в with(). Це правило легко перевіряється і дисциплінує команду. Якщо його дотримуватися, N+1 практично зникає.
Окрема тема — підвантаження “вглиб”. Якщо ти показуєш новини, автора і роль автора, ти можеш робити вкладене eager loading. Тобто підвантажити user і всередині нього role. Це знову ж таки зберігає кількість запитів контрольованою, а код в шаблоні стає чистим.
Ще одна важлива річ для списків — withCount(). Дуже часто в адмінці треба показати “скільки” пов’язаних записів: скільки новин у теми, скільки позицій у рахунку, скільки коментарів у статті. Замість того щоб тягнути всю колекцію і рахувати в PHP, ти робиш withCount, і база віддає число. Це швидко і правильно для масштабування.
belongsToMany має свої практичні моменти. Для тегів/категорій часто потрібні операції attach, detach, sync. Важливе правило: такі операції часто мають йти в транзакції разом з основним записом, якщо ти оновлюєш і саму новину, і її теги. Інакше можна отримати стан “новина оновилась, а теги — ні” при помилці на середині.
Щоб не ловити продуктивні проблеми, треба виробити просту звичку контролю. Коли сторінка стала “думати”, дивись не на Blade, а на кількість запитів і їх форму. N+1 зазвичай видно одразу: багато однакових запитів до однієї таблиці з різними id. У Laravel це легко відловлюється через логи або інструменти типу Telescope, але навіть без них ти можеш побачити закономірність по поведінці.
Правильний мінімальний стандарт для зв’язків у проєкті такий. У моделях чітко описані belongsTo/hasMany/belongsToMany, назви зв’язків читаються як сутності (user, author, tags, sections), foreign keys узгоджені типами та індексами. У контролерах для списків завжди є with() або withCount() під реальний UI. У Blade ніколи не робиться “підвантаження на льоту” без eager loading, інакше це гарантований N+1 при зростанні даних.
Висновок: зв’язки Eloquent — це основа читабельного і швидкого Laravel-коду. belongsTo показує, кому належить запис, hasMany дає доступ до дочірніх сутностей, belongsToMany вирішує зв’язки через pivot. А eager loading — це інструмент, який не дозволяє спискам перетворитися на сотні запитів. Якщо ти зробиш ці правила стандартом з першого місяця, твоя адмінка буде працювати швидко навіть тоді, коли даних стане в 50 разів більше.