Laravel: зв'язки Eloquent і N+1
20 питань · ~20 хв · Версія v3.0
Увійдіть, щоб продовжити
Усі типи зв'язків, поліморфні й pivot, запити за наявністю, жадібне завантаження та N+1, збереження пов'язаних моделей - питання всіх рівнів, від junior до senior.
- За спробу
- 20
- У пулі
- 46
- Проходжень
- 0
- Середній бал
- -
- Пройшли на 70%+
- -
Питання для підготовки
10 питаньN+1 виникає, коли ви завантажуєте N моделей одним запитом, а потім у циклі звертаєтесь до їхнього зв'язку - це генерує ще N запитів.
$posts = Post::all(); // 1 запит
foreach ($posts as $post) {
echo $post->author->name; // +1 запит на кожен пост → N запитів
}
Рішення - eager loading через with():
$posts = Post::with('author')->get(); // лише 2 запити загалом
Вкладені та умовні зв'язки:
Post::with(['author', 'comments.user'])->get(); // вкладений eager
Post::with(['comments' => fn ($q) => $q->latest()])->get(); // умовний
Лічильники без завантаження зв'язку - withCount() (без N+1 і без вантаження самих рядків):
$posts = Post::withCount('comments')->get(); // доступ через $post->comments_count
Виявлення: Model::preventLazyLoading(! app()->isProduction()) у boot() кидає виняток на ледачих завантаженнях поза продакшеном; також допомагають Telescope і Debugbar.
Eloquent підтримує всі поширені типи зв'язків між таблицями, кожен оголошується методом на моделі:
- One To One -
hasOne/belongsTo. Приклад:User↔Profile. - One To Many -
hasMany/belongsTo. Приклад:Post→ багатоComment. - Many To Many -
belongsToManyчерез проміжну (pivot) таблицю. Приклад:User↔Role. - Has One/Many Through - доступ до віддаленого зв'язку через проміжну модель.
- Polymorphic -
morphTo/morphMany: модель належить кільком типам (наприклад,Commentможе належати іPost, іVideo).
Оголошення зв'язку:
class Post extends Model
{
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}
class Comment extends Model
{
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}
Використання:
$post->comments; // колекція коментарів
$comment->post->title; // зворотний бік
Post::with('comments')->get(); // eager loading проти N+1
Завжди завантажуйте потрібні зв'язки через with(), щоб уникнути проблеми N+1.
Поліморфний зв'язок дозволяє моделі належати кільком різним типам моделей через один зв'язок.
class Comment extends Model
{
public function commentable(): MorphTo
{
return $this->morphTo();
}
}
class Post extends Model
{
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
Таблиця comments має commentable_id + commentable_type. Тож Comment може належати і Post, і Video без окремих таблиць. Бувають також many-to-many поліморфні зв'язки (morphToMany), напр. теги.
У belongsToMany проміжна (pivot) таблиця зберігає зв'язки. Додаткові стовпці на ній оголошують через withPivot():
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class)
->withPivot('assigned_at', 'is_primary')
->withTimestamps();
}
Доступ і керування:
$user->roles->first()->pivot->assigned_at; // читання pivot-даних
$user->roles()->attach($roleId, ['is_primary' => true]); // додати
$user->roles()->detach($roleId); // прибрати
$user->roles()->sync([1, 2, 3]);// привести до набору
$user->roles()->updateExistingPivot($roleId, [...]); // оновити pivot
Для окремої моделі pivot використовують ->using(RoleUser::class).
Зовнішній ключ завжди лежить у таблиці тієї моделі, що належить іншій. Звідси й вибір методу.
// posts.user_id посилається на users.id
class User extends Model
{
public function posts(): HasMany
{
return $this->hasMany(Post::class); // ключ у чужій таблиці
}
}
class Post extends Model
{
public function user(): BelongsTo
{
return $this->belongsTo(User::class); // ключ у своїй таблиці
}
}
Просте правило: у якій моделі колонка *_id, та модель і пише belongsTo. Інша сторона - hasOne чи hasMany.
Імена за замовчуванням:
hasManyшукає вpostsколонкуuser_id- ім'я моделі-власника в snake_case плюс_id;belongsToбере ім'я методу плюс_id: методauthor()шукатимеauthor_id, і тоді ключ доведеться передати явно, якщо колонка називається інакше.
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
Часта помилка новачків - написати hasOne там, де потрібен belongsTo, бо «в пості є один автор». Запит тоді шукатиме users.post_id, якого не існує.
Спершу почитати
Прочитати - ще не значить знати
20 питань, по одному на екран, ~20 хв. Після завершення - розбір кожної помилки з посиланням на питання.