Отношения Eloquent: связи между таблицами
Отношения Eloquent описывают связи между таблицами PHP-методами, позволяя обращаться к связанным данным как к свойствам объекта.
Суть: у пользователя много заказов (
hasMany), заказ принадлежит пользователю (belongsTo), у поста много тегов и наоборот (belongsToMany). Eloquent сам строит JOIN-запросы.
В реальной базе данные связаны: у автора много статей, у заказа — много товаров, у товара — категория. В чистом SQL это означает писать JOIN-запросы вручную. Eloquent делает связи декларативными: вы описываете тип отношения методом в модели, а потом обращаетесь к связанным записям как к обычному свойству. Например, $user->orders вернёт все заказы пользователя.
Три основных типа отношений покрывают большинство случаев. Один-ко-многим: у пользователя много заказов. Обратное: заказ принадлежит пользователю. Многие-ко-многим: у поста много тегов, и каждый тег относится ко многим постам (через промежуточную таблицу).
Описание отношений
<?php
// User.php — у пользователя много заказов
class User extends Model
{
public function orders()
{
return $this->hasMany(Order::class);
}
}
// Order.php — заказ принадлежит пользователю
class Order extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}
// Post.php — многие-ко-многим с тегами
class Post extends Model
{
public function tags()
{
return $this->belongsToMany(Tag::class);
}
}Обращение к связанным данным выглядит как обращение к свойству:
<?php
$user = User::find(1);
foreach ($user->orders as $order) { // все заказы пользователя
echo $order->total;
}
$order = Order::find(5);
echo $order->user->name; // имя владельца заказаКак работает под капотом и проблема N+1
Связь определяется по соглашению о внешних ключах: hasMany(Order::class) ищет в таблице orders колонку user_id. При первом обращении к $user->orders Eloquent выполняет отдельный SQL-запрос (ленивая загрузка). И тут возникает классическая ловушка — проблема N+1: если в цикле по 100 пользователям обращаться к их заказам, выполнится 1 запрос на пользователей плюс 100 запросов на заказы.
ПРОБЛЕМА N+1
-----------
User::all() -> 1 запрос (100 пользователей)
foreach -> $u->orders -> +100 запросов!
ИТОГО: 101 запрос (медленно)
РЕШЕНИЕ: жадная загрузка with()
-------------------------------
User::with('orders')->get()
-> 2 запроса: пользователи + все их заказы разом
Лечится жадной загрузкой with():
<?php
// вместо 101 запроса — всего 2
$users = User::with('orders')->get();Смоделируем разницу между N+1 и жадной загрузкой на Python — считаем количество «запросов».
Попробуй сам ▶
# Сравнение: ленивая (N+1) против жадной загрузки
users = [{'id': i, 'name': f'U{i}'} for i in range(1, 6)]
orders = {1: ['A'], 2: ['B', 'C'], 3: [], 4: ['D'], 5: ['E', 'F']}
# N+1: запрос на пользователей + по запросу на каждого
q = 1
for u in users:
_ = orders[u['id']] # отдельный запрос
q += 1
print('Ленивая загрузка: запросов =', q)
# Жадная with(): 2 запроса всего
print('Жадная with(): запросов =', 2)
Частые ошибки
- Проблема N+1. Обращение к связям в цикле без
with()рождает лавину запросов и тормозит страницу. - Неверный внешний ключ. Если колонка называется не по соглашению, нужно указать её явно вторым аргументом.
- Забыть промежуточную таблицу. Для
belongsToManyнужна сводная таблица (например,post_tag).
Best practices
- Всегда используйте
with()для связей, к которым обращаетесь в цикле. - Называйте внешние ключи по соглашению (
user_id) — меньше ручной настройки. - Используйте
php artisan model:showили дебаг-панель, чтобы ловить N+1.
Отношения в Eloquent — это не только чтение связанных данных, но и удобное их создание. Метод save() на отношении автоматически проставит внешний ключ: вызов $user->orders()->create(['total' => 500]) создаст заказ и сразу привяжет его к пользователю, не заставляя вручную указывать user_id. Для связей многие-ко-многим есть методы attach(), detach() и sync(), управляющие записями в промежуточной таблице: $post->tags()->sync([1, 2, 3]) приведёт набор тегов поста ровно к указанному списку. Кроме того, отношения можно использовать как условия в запросах: User::has('orders') вернёт только пользователей с заказами, а User::whereHas('orders', fn($q) => $q->where('total', '>', 1000)) — тех, у кого есть крупные заказы. Это позволяет фильтровать по связанным данным, не выгружая их в память.
Итог: отношения Eloquent делают связи между таблицами декларативными, а жадная загрузка спасает от проблемы N+1. Дальше переходим к работе с формами и валидацией пользовательского ввода.