Звʼязки · Laravel
Вступ
Таблиці бази даних часто повʼязані одна з одною. Наприклад, допис у блозі може мати багато коментарів, а замовлення може бути повʼязане з користувачем, який його зробив. Eloquent робить керування та роботу з такими звʼязками простою і підтримує різні поширені звʼязки:
- Один до одного
- Один до багатьох
- Багато до багатьох
- Has One Through
- Has Many Through
- Один до одного (поліморфний)
- Один до багатьох (поліморфний)
- Багато до багатьох (поліморфний)
Визначення звʼязків
Звʼязки Eloquent визначаються як методи ваших класів моделей Eloquent. Оскільки звʼязки також слугують потужними конструкторами запитів, визначення звʼязків як методів надає потужні можливості ланцюжкового виклику методів і побудови запитів. Наприклад, ми можемо додати в ланцюжок додаткові обмеження запиту до цього звʼязку posts:
$user->posts()->where('active', 1)->get();
Але перш ніж занурюватися надто глибоко у використання звʼязків, розгляньмо, як визначити кожен тип звʼязку, який підтримує Eloquent.
Один до одного / Has One
Звʼязок «один до одного» — це дуже базовий тип звʼязку в базі даних. Наприклад, модель User може бути повʼязана з однією моделлю Phone. Щоб визначити цей звʼязок, ми розмістимо метод phone у моделі User. Метод phone має викликати метод hasOne і повернути його результат. Метод hasOne доступний вашій моделі через базовий клас моделі Illuminate\Database\Eloquent\Model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
class User extends Model
{
/**
* Отримати телефон, повʼязаний із користувачем.
*/
public function phone(): HasOne
{
return $this->hasOne(Phone::class);
}
}
Перший аргумент, переданий методу hasOne, — це імʼя класу повʼязаної моделі. Щойно звʼязок визначено, ми можемо отримати повʼязаний запис за допомогою динамічних властивостей Eloquent. Динамічні властивості дозволяють звертатися до методів звʼязків так, ніби вони є властивостями, визначеними в моделі:
$phone = User::find(1)->phone;
Eloquent визначає зовнішній ключ звʼязку на основі імені батьківської моделі. У цьому випадку автоматично припускається, що модель Phone має зовнішній ключ user_id. Якщо ви хочете перевизначити цю домовленість, ви можете передати другий аргумент методу hasOne:
return $this->hasOne(Phone::class, 'foreign_key');
Крім того, Eloquent припускає, що зовнішній ключ повинен мати значення, яке відповідає колонці первинного ключа батьківської моделі. Іншими словами, Eloquent шукатиме значення колонки id користувача в колонці user_id запису Phone. Якщо ви хочете, щоб звʼязок використовував значення первинного ключа, відмінне від id чи первинного ключа вашої моделі, ви можете передати третій аргумент методу hasOne:
return $this->hasOne(Phone::class, 'foreign_key', 'local_key');
Визначення зворотного боку звʼязку
Отже, ми можемо звертатися до моделі Phone з нашої моделі User. Далі визначмо звʼязок у моделі Phone, який дозволить нам отримати користувача, якому належить телефон. Ми можемо визначити зворотний бік звʼязку hasOne за допомогою методу belongsTo:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Phone extends Model
{
/**
* Отримати користувача, якому належить телефон.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
Під час виклику методу user Eloquent намагатиметься знайти модель User, чий id відповідає колонці user_id у моделі Phone.
Eloquent визначає імʼя зовнішнього ключа, аналізуючи імʼя методу звʼязку і додаючи до імені методу суфікс _id. Отже, у цьому випадку Eloquent припускає, що модель Phone має колонку user_id. Однак, якщо зовнішній ключ у моделі Phone не user_id, ви можете передати власне імʼя ключа другим аргументом методу belongsTo:
/**
* Отримати користувача, якому належить телефон.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key');
}
Якщо батьківська модель не використовує id як первинний ключ або ви хочете знайти повʼязану модель за іншою колонкою, ви можете передати третій аргумент методу belongsTo, вказавши власний ключ батьківської таблиці:
/**
* Отримати користувача, якому належить телефон.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
}
Один до багатьох / Has Many
Звʼязок «один до багатьох» використовується для визначення звʼязків, де одна модель є батьківською для однієї або кількох дочірніх моделей. Наприклад, допис у блозі може мати нескінченну кількість коментарів. Як і всі інші звʼязки Eloquent, звʼязки «один до багатьох» визначаються шляхом оголошення методу у вашій моделі Eloquent:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Post extends Model
{
/**
* Отримати коментарі до допису в блозі.
*/
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}
Памʼятайте, Eloquent автоматично визначить відповідну колонку зовнішнього ключа для моделі Comment. За домовленістю Eloquent візьме імʼя батьківської моделі у «snake case» і додасть до нього суфікс _id. Отже, у цьому прикладі Eloquent припускатиме, що колонка зовнішнього ключа в моделі Comment — це post_id.
Щойно метод звʼязку визначено, ми можемо звертатися до колекції повʼязаних коментарів через властивість comments. Памʼятайте: оскільки Eloquent надає «динамічні властивості звʼязків», ми можемо звертатися до методів звʼязків так, ніби вони визначені як властивості моделі:
use App\Models\Post;
$comments = Post::find(1)->comments;
foreach ($comments as $comment) {
// ...
}
Оскільки всі звʼязки також слугують конструкторами запитів, ви можете додати додаткові обмеження до запиту звʼязку, викликавши метод comments і продовживши ланцюжок умов до запиту:
$comment = Post::find(1)->comments()
->where('title', 'foo')
->first();
Як і в методі hasOne, ви також можете перевизначити зовнішній і локальний ключі, передавши додаткові аргументи методу hasMany:
return $this->hasMany(Comment::class, 'foreign_key');
return $this->hasMany(Comment::class, 'foreign_key', 'local_key');
Автоматичне наповнення батьківських моделей у дочірніх
Навіть при використанні жадібного завантаження Eloquent проблеми запитів «N + 1» можуть виникнути, якщо ви намагаєтеся звернутися до батьківської моделі з дочірньої моделі під час перебору дочірніх моделей:
$posts = Post::with('comments')->get();
foreach ($posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->post->title;
}
}
У наведеному вище прикладі виникла проблема запитів «N + 1», тому що, хоча коментарі і були жадібно завантажені для кожної моделі Post, Eloquent не наповнює автоматично батьківський Post у кожній дочірній моделі Comment.
Якщо ви хочете, щоб Eloquent автоматично наповнював батьківські моделі в їхніх дочірніх, ви можете викликати метод chaperone під час визначення звʼязку hasMany:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Post extends Model
{
/**
* Отримати коментарі до допису в блозі.
*/
public function comments(): HasMany
{
return $this->hasMany(Comment::class)->chaperone();
}
}
Або, якщо ви хочете ввімкнути автоматичне наповнення батьківської моделі під час виконання, ви можете викликати метод chaperone під час жадібного завантаження звʼязку:
use App\Models\Post;
$posts = Post::with([
'comments' => fn ($comments) => $comments->chaperone(),
])->get();
Один до багатьох (зворотний) / Belongs To
Тепер, коли ми можемо звертатися до всіх коментарів допису, визначмо звʼязок, який дозволить коментарю звертатися до свого батьківського допису. Щоб визначити зворотний бік звʼязку hasMany, визначте метод звʼязку в дочірній моделі, який викликає метод belongsTo:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Comment extends Model
{
/**
* Отримати допис, якому належить коментар.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}
Щойно звʼязок визначено, ми можемо отримати батьківський допис коментаря, звернувшись до «динамічної властивості звʼязку» post:
use App\Models\Comment;
$comment = Comment::find(1);
return $comment->post->title;
У наведеному вище прикладі Eloquent намагатиметься знайти модель Post, чий id відповідає колонці post_id у моделі Comment.
Eloquent визначає типове імʼя зовнішнього ключа, аналізуючи імʼя методу звʼязку і додаючи до імені методу _, за яким іде імʼя колонки первинного ключа батьківської моделі. Отже, у цьому прикладі Eloquent припускатиме, що зовнішній ключ моделі Post у таблиці comments — це post_id.
Однак, якщо зовнішній ключ вашого звʼязку не відповідає цим домовленостям, ви можете передати власне імʼя зовнішнього ключа другим аргументом методу belongsTo:
/**
* Отримати допис, якому належить коментар.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key');
}
Якщо ваша батьківська модель не використовує id як первинний ключ або ви хочете знайти повʼязану модель за іншою колонкою, ви можете передати третій аргумент методу belongsTo, вказавши власний ключ батьківської таблиці:
/**
* Отримати допис, якому належить коментар.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');
}
Типові моделі
Звʼязки belongsTo, hasOne, hasOneThrough і morphOne дозволяють визначити типову модель, яка буде повернута, якщо вказаний звʼязок дорівнює null. Цей патерн часто називають патерном Null Object, і він може допомогти позбутися умовних перевірок у вашому коді. У наступному прикладі звʼязок user поверне порожню модель App\Models\User, якщо до моделі Post не привʼязано жодного користувача:
/**
* Отримати автора допису.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault();
}
Щоб наповнити типову модель атрибутами, ви можете передати масив або замикання методу withDefault:
/**
* Отримати автора допису.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault([
'name' => 'Guest Author',
]);
}
/**
* Отримати автора допису.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault(function (User $user, Post $post) {
$user->name = 'Guest Author';
});
}
Запити до звʼязків Belongs To
Роблячи запит до дочірніх моделей звʼязку «belongs to», ви можете вручну побудувати умову where, щоб отримати відповідні моделі Eloquent:
use App\Models\Post;
$posts = Post::where('user_id', $user->id)->get();
Однак вам може бути зручніше використати метод whereBelongsTo, який автоматично визначить правильний звʼязок і зовнішній ключ для вказаної моделі:
$posts = Post::whereBelongsTo($user)->get();
Ви також можете передати методу whereBelongsTo екземпляр колекції. У такому разі Laravel отримає моделі, що належать будь-якій із батьківських моделей у цій колекції:
$users = User::where('vip', true)->get();
$posts = Post::whereBelongsTo($users)->get();
За замовчуванням Laravel визначить звʼязок, повʼязаний із вказаною моделлю, на основі імені класу моделі; однак ви можете вказати імʼя звʼязку вручну, передавши його другим аргументом методу whereBelongsTo:
$posts = Post::whereBelongsTo($user, 'author')->get();
Has One of Many
Інколи модель може мати багато повʼязаних моделей, але ви хочете легко отримувати «найновішу» або «найстарішу» повʼязану модель звʼязку. Наприклад, модель User може бути повʼязана з багатьма моделями Order, але ви хочете визначити зручний спосіб роботи з найновішим замовленням, яке зробив користувач. Ви можете зробити це за допомогою типу звʼязку hasOne у поєднанні з методами ofMany:
/**
* Отримати найновіше замовлення користувача.
*/
public function latestOrder(): HasOne
{
return $this->hasOne(Order::class)->latestOfMany();
}
Так само ви можете визначити метод для отримання «найстарішої», або першої, повʼязаної моделі звʼязку:
/**
* Отримати найстаріше замовлення користувача.
*/
public function oldestOrder(): HasOne
{
return $this->hasOne(Order::class)->oldestOfMany();
}
За замовчуванням методи latestOfMany і oldestOfMany отримуватимуть найновішу або найстарішу повʼязану модель на основі первинного ключа моделі, який має бути придатним для сортування. Однак інколи ви можете захотіти отримати одну модель із більшого звʼязку за іншим критерієм сортування.
Наприклад, за допомогою методу ofMany ви можете отримати найдорожче замовлення користувача. Метод ofMany приймає придатну для сортування колонку першим аргументом і те, яку агрегатну функцію (min чи max) застосувати під час запиту повʼязаної моделі:
/**
* Отримати найбільше замовлення користувача.
*/
public function largestOrder(): HasOne
{
return $this->hasOne(Order::class)->ofMany('price', 'max');
}
[!WARNING] Оскільки PostgreSQL не підтримує виконання функції
MAXнад колонками UUID, наразі неможливо використовувати звʼязки one-of-many в поєднанні з колонками UUID у PostgreSQL.
Перетворення звʼязків «Many» на звʼязки Has One
Часто, отримуючи одну модель за допомогою методів latestOfMany, oldestOfMany або ofMany, ви вже маєте визначений звʼязок «has many» для тієї самої моделі. Для зручності Laravel дозволяє легко перетворити цей звʼязок на звʼязок «has one», викликавши метод one для звʼязку:
/**
* Отримати замовлення користувача.
*/
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
/**
* Отримати найбільше замовлення користувача.
*/
public function largestOrder(): HasOne
{
return $this->orders()->one()->ofMany('price', 'max');
}
Ви також можете використовувати метод one, щоб перетворити звʼязки HasManyThrough на звʼязки HasOneThrough:
public function latestDeployment(): HasOneThrough
{
return $this->deployments()->one()->latestOfMany();
}
Складніші звʼязки Has One of Many
Можна побудувати складніші звʼязки «has one of many». Наприклад, модель Product може мати багато повʼязаних моделей Price, які зберігаються в системі навіть після публікації нових цін. Крім того, нові дані про ціни для продукту можуть публікуватися заздалегідь, щоб набути чинності в майбутню дату через колонку published_at.
Отже, підсумовуючи, нам потрібно отримати останню опубліковану ціну, де дата публікації не в майбутньому. Крім того, якщо дві ціни мають однакову дату публікації, ми віддамо перевагу ціні з найбільшим ID. Щоб зробити це, ми маємо передати методу ofMany масив, який містить придатні для сортування колонки, що визначають останню ціну. Крім того, другим аргументом методу ofMany буде передано замикання. Це замикання відповідатиме за додавання додаткових обмежень щодо дати публікації до запиту звʼязку:
/**
* Отримати поточну ціну продукту.
*/
public function currentPricing(): HasOne
{
return $this->hasOne(Price::class)->ofMany([
'published_at' => 'max',
'id' => 'max',
], function (Builder $query) {
$query->where('published_at', '<', now());
});
}
Has One Through
Звʼязок «has-one-through» визначає звʼязок «один до одного» з іншою моделлю. Однак цей звʼязок вказує, що модель, яка його оголошує, може бути зіставлена з одним екземпляром іншої моделі, проходячи через третю модель.
Наприклад, у застосунку автомайстерні кожна модель Mechanic може бути повʼязана з однією моделлю Car, а кожна модель Car може бути повʼязана з однією моделлю Owner. Хоча механік і власник не мають прямого звʼязку в базі даних, механік може звертатися до власника через модель Car. Розгляньмо таблиці, потрібні для визначення цього звʼязку:
mechanics
id - integer
name - string
cars
id - integer
model - string
mechanic_id - integer
owners
id - integer
name - string
car_id - integer
Тепер, коли ми розглянули структуру таблиць для звʼязку, визначмо звʼязок у моделі Mechanic:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
class Mechanic extends Model
{
/**
* Отримати власника автомобіля.
*/
public function carOwner(): HasOneThrough
{
return $this->hasOneThrough(Owner::class, Car::class);
}
}
Перший аргумент, переданий методу hasOneThrough, — це імʼя кінцевої моделі, до якої ми хочемо звертатися, а другий аргумент — імʼя проміжної моделі.
Або, якщо відповідні звʼязки вже визначені в усіх моделях, задіяних у звʼязку, ви можете плавно визначити звʼязок «has-one-through», викликавши метод through і вказавши імена цих звʼязків. Наприклад, якщо модель Mechanic має звʼязок cars, а модель Car має звʼязок owner, ви можете визначити звʼязок «has-one-through», що зʼєднує механіка і власника, так:
// Рядковий синтаксис...
return $this->through('cars')->has('owner');
// Динамічний синтаксис...
return $this->throughCars()->hasOwner();
Домовленості щодо ключів
Під час виконання запитів звʼязку використовуються типові домовленості Eloquent щодо зовнішніх ключів. Якщо ви хочете налаштувати ключі звʼязку, ви можете передати їх третім і четвертим аргументами методу hasOneThrough. Третій аргумент — це імʼя зовнішнього ключа в проміжній моделі. Четвертий аргумент — імʼя зовнішнього ключа в кінцевій моделі. Пʼятий аргумент — локальний ключ, а шостий аргумент — локальний ключ проміжної моделі:
class Mechanic extends Model
{
/**
* Отримати власника автомобіля.
*/
public function carOwner(): HasOneThrough
{
return $this->hasOneThrough(
Owner::class,
Car::class,
'mechanic_id', // Зовнішній ключ у таблиці cars...
'car_id', // Зовнішній ключ у таблиці owners...
'id', // Локальний ключ у таблиці mechanics...
'id' // Локальний ключ у таблиці cars...
);
}
}
Або, як обговорювалося раніше, якщо відповідні звʼязки вже визначені в усіх моделях, задіяних у звʼязку, ви можете плавно визначити звʼязок «has-one-through», викликавши метод through і вказавши імена цих звʼязків. Цей підхід має перевагу повторного використання домовленостей щодо ключів, уже визначених у наявних звʼязках:
// Рядковий синтаксис...
return $this->through('cars')->has('owner');
// Динамічний синтаксис...
return $this->throughCars()->hasOwner();
Has Many Through
Звʼязок «has-many-through» надає зручний спосіб звертатися до віддалених звʼязків через проміжний звʼязок. Наприклад, припустімо, що ми будуємо платформу для деплою на кшталт Laravel Cloud. Модель Application може звертатися до багатьох моделей Deployment через проміжну модель Environment. У цьому прикладі ви могли б легко зібрати всі деплої для заданого застосунку. Розгляньмо таблиці, потрібні для визначення цього звʼязку:
applications
id - integer
name - string
environments
id - integer
application_id - integer
name - string
deployments
id - integer
environment_id - integer
commit_hash - string
Тепер, коли ми розглянули структуру таблиць для звʼязку, визначмо звʼязок у моделі Application:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
class Application extends Model
{
/**
* Отримати всі деплої застосунку.
*/
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(Deployment::class, Environment::class);
}
}
Перший аргумент, переданий методу hasManyThrough, — це імʼя кінцевої моделі, до якої ми хочемо звертатися, а другий аргумент — імʼя проміжної моделі.
Або, якщо відповідні звʼязки вже визначені в усіх моделях, задіяних у звʼязку, ви можете плавно визначити звʼязок «has-many-through», викликавши метод through і вказавши імена цих звʼязків. Наприклад, якщо модель Application має звʼязок environments, а модель Environment має звʼязок deployments, ви можете визначити звʼязок «has-many-through», що зʼєднує застосунок і деплої, так:
// Рядковий синтаксис...
return $this->through('environments')->has('deployments');
// Динамічний синтаксис...
return $this->throughEnvironments()->hasDeployments();
Хоча таблиця моделі Deployment не містить колонки application_id, звʼязок hasManyThrough надає доступ до деплоїв застосунку через $application->deployments. Щоб отримати ці моделі, Eloquent перевіряє колонку application_id у таблиці проміжної моделі Environment. Після знаходження відповідних ID середовищ вони використовуються для запиту до таблиці моделі Deployment.
Домовленості щодо ключів
Під час виконання запитів звʼязку використовуються типові домовленості Eloquent щодо зовнішніх ключів. Якщо ви хочете налаштувати ключі звʼязку, ви можете передати їх третім і четвертим аргументами методу hasManyThrough. Третій аргумент — це імʼя зовнішнього ключа в проміжній моделі. Четвертий аргумент — імʼя зовнішнього ключа в кінцевій моделі. Пʼятий аргумент — локальний ключ, а шостий аргумент — локальний ключ проміжної моделі:
class Application extends Model
{
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(
Deployment::class,
Environment::class,
'application_id', // Зовнішній ключ у таблиці environments...
'environment_id', // Зовнішній ключ у таблиці deployments...
'id', // Локальний ключ у таблиці applications...
'id' // Локальний ключ у таблиці environments...
);
}
}
Або, як обговорювалося раніше, якщо відповідні звʼязки вже визначені в усіх моделях, задіяних у звʼязку, ви можете плавно визначити звʼязок «has-many-through», викликавши метод through і вказавши імена цих звʼязків. Цей підхід має перевагу повторного використання домовленостей щодо ключів, уже визначених у наявних звʼязках:
// Рядковий синтаксис...
return $this->through('environments')->has('deployments');
// Динамічний синтаксис...
return $this->throughEnvironments()->hasDeployments();
Звʼязки з обмеженнями (scoped)
Часто до моделей додають додаткові методи, які обмежують звʼязки. Наприклад, ви можете додати метод featuredPosts до моделі User, який обмежує ширший звʼязок posts додатковою умовою where:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Model
{
/**
* Отримати дописи користувача.
*/
public function posts(): HasMany
{
return $this->hasMany(Post::class)->latest();
}
/**
* Отримати рекомендовані дописи користувача.
*/
public function featuredPosts(): HasMany
{
return $this->posts()->where('featured', true);
}
}
Однак, якщо ви спробуєте створити модель через метод featuredPosts, її атрибут featured не буде встановлено в true. Якщо ви хочете створювати моделі через методи звʼязків і також вказати атрибути, які слід додавати до всіх моделей, створених через цей звʼязок, ви можете використати метод withAttributes під час побудови запиту звʼязку:
/**
* Отримати рекомендовані дописи користувача.
*/
public function featuredPosts(): HasMany
{
return $this->posts()->withAttributes(['featured' => true]);
}
Метод withAttributes додасть до запиту умови where з використанням вказаних атрибутів, а також додасть вказані атрибути до будь-яких моделей, створених через метод звʼязку:
$post = $user->featuredPosts()->create(['title' => 'Featured Post']);
$post->featured; // true
Щоб вказати методу withAttributes не додавати умови where до запиту, ви можете встановити аргумент asConditions у false:
return $this->posts()->withAttributes(['featured' => true], asConditions: false);
Звʼязки «багато до багатьох»
Звʼязки «багато до багатьох» дещо складніші за звʼязки hasOne і hasMany. Прикладом звʼязку «багато до багатьох» є користувач, який має багато ролей, і ці ролі також спільні з іншими користувачами в застосунку. Наприклад, користувачу можуть бути призначені ролі «Author» та «Editor»; однак ці ролі можуть бути призначені й іншим користувачам. Отже, користувач має багато ролей, а роль має багато користувачів.
Структура таблиць
Щоб визначити цей звʼязок, потрібні три таблиці бази даних: users, roles і role_user. Імʼя таблиці role_user походить від алфавітного порядку імен повʼязаних моделей і містить колонки user_id та role_id. Ця таблиця використовується як проміжна таблиця, що звʼязує користувачів і ролі.
Памʼятайте: оскільки роль може належати багатьом користувачам, ми не можемо просто розмістити колонку user_id у таблиці roles. Це означало б, що роль може належати лише одному користувачу. Щоб забезпечити підтримку призначення ролей кільком користувачам, потрібна таблиця role_user. Ми можемо підсумувати структуру таблиць звʼязку так:
users
id - integer
name - string
roles
id - integer
name - string
role_user
user_id - integer
role_id - integer
Структура моделей
Звʼязки «багато до багатьох» визначаються написанням методу, який повертає результат методу belongsToMany. Метод belongsToMany надається базовим класом Illuminate\Database\Eloquent\Model, який використовують усі моделі Eloquent вашого застосунку. Наприклад, визначмо метод roles у нашій моделі User. Перший аргумент, переданий цьому методу, — це імʼя класу повʼязаної моделі:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class User extends Model
{
/**
* Ролі, що належать користувачу.
*/
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
}
Щойно звʼязок визначено, ви можете звертатися до ролей користувача за допомогою динамічної властивості звʼязку roles:
use App\Models\User;
$user = User::find(1);
foreach ($user->roles as $role) {
// ...
}
Оскільки всі звʼязки також слугують конструкторами запитів, ви можете додати додаткові обмеження до запиту звʼязку, викликавши метод roles і продовживши ланцюжок умов до запиту:
$roles = User::find(1)->roles()->orderBy('name')->get();
Щоб визначити імʼя проміжної таблиці звʼязку, Eloquent зʼєднає імена двох повʼязаних моделей в алфавітному порядку. Однак ви можете вільно перевизначити цю домовленість. Ви можете зробити це, передавши другий аргумент методу belongsToMany:
return $this->belongsToMany(Role::class, 'role_user');
Крім налаштування імені проміжної таблиці, ви також можете налаштувати імена колонок ключів у таблиці, передавши додаткові аргументи методу belongsToMany. Третій аргумент — це імʼя зовнішнього ключа моделі, у якій ви визначаєте звʼязок, а четвертий аргумент — імʼя зовнішнього ключа моделі, з якою ви зʼєднуєтеся:
return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');
Визначення зворотного боку звʼязку
Щоб визначити «зворотний» бік звʼязку «багато до багатьох», вам слід визначити метод у повʼязаній моделі, який також повертає результат методу belongsToMany. Щоб завершити наш приклад із користувачем / роллю, визначмо метод users у моделі Role:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Role extends Model
{
/**
* Користувачі, що належать до ролі.
*/
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
}
Як бачите, звʼязок визначається точно так само, як його відповідник у моделі User, за винятком посилання на модель App\Models\User. Оскільки ми повторно використовуємо метод belongsToMany, усі звичайні опції налаштування таблиці та ключів доступні під час визначення «зворотного» боку звʼязків «багато до багатьох».
Отримання колонок проміжної таблиці
Як ви вже дізналися, робота зі звʼязками «багато до багатьох» вимагає наявності проміжної таблиці. Eloquent надає кілька дуже корисних способів взаємодії з цією таблицею. Наприклад, припустімо, що наша модель User має багато повʼязаних із нею моделей Role. Після звернення до цього звʼязку ми можемо звертатися до проміжної таблиці за допомогою атрибута pivot у моделях:
use App\Models\User;
$user = User::find(1);
foreach ($user->roles as $role) {
echo $role->pivot->created_at;
}
Зверніть увагу, що кожній отриманій моделі Role автоматично призначається атрибут pivot. Цей атрибут містить модель, що представляє проміжну таблицю.
За замовчуванням у моделі pivot будуть присутні лише ключі моделей. Якщо ваша проміжна таблиця містить додаткові атрибути, ви маєте вказати їх під час визначення звʼязку:
return $this->belongsToMany(Role::class)->withPivot('active', 'created_by');
Якщо ви хочете, щоб ваша проміжна таблиця мала часові мітки created_at та updated_at, які автоматично підтримуються Eloquent, викличте метод withTimestamps під час визначення звʼязку:
return $this->belongsToMany(Role::class)->withTimestamps();
[!WARNING] Проміжні таблиці, що використовують автоматично підтримувані Eloquent часові мітки, повинні мати обидві колонки часових міток
created_atтаupdated_at.
Налаштування імені атрибута pivot
Як зазначалося раніше, до атрибутів із проміжної таблиці можна звертатися в моделях через атрибут pivot. Однак ви можете вільно налаштувати імʼя цього атрибута, щоб краще відображати його призначення у вашому застосунку.
Наприклад, якщо ваш застосунок містить користувачів, які можуть підписуватися на подкасти, ви, імовірно, маєте звʼязок «багато до багатьох» між користувачами і подкастами. Якщо це так, ви можете захотіти перейменувати атрибут проміжної таблиці на subscription замість pivot. Це можна зробити за допомогою методу as під час визначення звʼязку:
return $this->belongsToMany(Podcast::class)
->as('subscription')
->withTimestamps();
Щойно власний атрибут проміжної таблиці вказано, ви можете звертатися до даних проміжної таблиці за допомогою налаштованого імені:
$users = User::with('podcasts')->get();
foreach ($users->flatMap->podcasts as $podcast) {
echo $podcast->subscription->created_at;
}
Фільтрація запитів за колонками проміжної таблиці
Ви також можете фільтрувати результати, які повертають запити звʼязку belongsToMany, за допомогою методів wherePivot, wherePivotIn, wherePivotNotIn, wherePivotBetween, wherePivotNotBetween, wherePivotNull і wherePivotNotNull під час визначення звʼязку:
return $this->belongsToMany(Role::class)
->wherePivot('approved', 1);
return $this->belongsToMany(Role::class)
->wherePivotIn('priority', [1, 2]);
return $this->belongsToMany(Role::class)
->wherePivotNotIn('priority', [1, 2]);
return $this->belongsToMany(Podcast::class)
->as('subscriptions')
->wherePivotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);
return $this->belongsToMany(Podcast::class)
->as('subscriptions')
->wherePivotNotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);
return $this->belongsToMany(Podcast::class)
->as('subscriptions')
->wherePivotNull('expired_at');
return $this->belongsToMany(Podcast::class)
->as('subscriptions')
->wherePivotNotNull('expired_at');
Метод wherePivot додає обмеження where до запиту, але не додає вказане значення під час створення нових моделей через визначений звʼязок. Якщо вам потрібно і робити запити, і створювати звʼязки з певним значенням pivot, ви можете використати метод withPivotValue:
return $this->belongsToMany(Role::class)
->withPivotValue('approved', 1);
Сортування запитів за колонками проміжної таблиці
Ви можете сортувати результати, які повертають запити звʼязку belongsToMany, за допомогою методів orderByPivot та orderByPivotDesc. У наступному прикладі ми отримаємо всі найновіші значки користувача:
return $this->belongsToMany(Badge::class)
->where('rank', 'gold')
->orderByPivotDesc('created_at');
Визначення власних моделей проміжної таблиці
Якщо ви хочете визначити власну модель для представлення проміжної таблиці вашого звʼязку «багато до багатьох», ви можете викликати метод using під час визначення звʼязку. Власні pivot-моделі дають вам можливість визначити додаткову поведінку в pivot-моделі, як-от методи та приведення типів.
Власні pivot-моделі «багато до багатьох» мають розширювати клас Illuminate\Database\Eloquent\Relations\Pivot, а власні поліморфні pivot-моделі «багато до багатьох» — клас Illuminate\Database\Eloquent\Relations\MorphPivot. Наприклад, ми можемо визначити модель Role, яка використовує власну pivot-модель RoleUser:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Role extends Model
{
/**
* Користувачі, що належать до ролі.
*/
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class)->using(RoleUser::class);
}
}
Визначаючи модель RoleUser, вам слід розширити клас Illuminate\Database\Eloquent\Relations\Pivot:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
class RoleUser extends Pivot
{
// ...
}
[!WARNING] Pivot-моделі не можуть використовувати трейт
SoftDeletes. Якщо вам потрібно мʼяко видаляти pivot-записи, розгляньте можливість перетворення вашої pivot-моделі на повноцінну модель Eloquent.
Власні pivot-моделі та автоінкрементні ID
Якщо ви визначили звʼязок «багато до багатьох», який використовує власну pivot-модель, і ця pivot-модель має автоінкрементний первинний ключ, вам слід переконатися, що ваш клас власної pivot-моделі використовує атрибут Table зі значенням incrementing, встановленим у true:
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Relations\Pivot;
#[Table(incrementing: true)]
class RoleUser extends Pivot
{
// ...
}
Поліморфні звʼязки
Поліморфний звʼязок дозволяє дочірній моделі належати більш ніж одному типу моделей за допомогою єдиної асоціації. Наприклад, уявіть, що ви створюєте застосунок, який дозволяє користувачам ділитися дописами в блозі та відео. У такому застосунку модель Comment може належати як моделі Post, так і моделі Video.
Один до одного (поліморфний)
Структура таблиць
Поліморфний звʼязок «один до одного» подібний до типового звʼязку «один до одного»; однак дочірня модель може належати більш ніж одному типу моделей за допомогою єдиної асоціації. Наприклад, Post у блозі та User можуть мати спільний поліморфний звʼязок із моделлю Image. Використання поліморфного звʼязку «один до одного» дозволяє мати єдину таблицю унікальних зображень, які можуть бути повʼязані з дописами та користувачами. Спочатку розгляньмо структуру таблиць:
posts
id - integer
name - string
users
id - integer
name - string
images
id - integer
url - string
imageable_type - string
imageable_id - integer
Зверніть увагу на колонки imageable_id та imageable_type у таблиці images. Колонка imageable_id міститиме значення ID допису або користувача, а колонка imageable_type міститиме імʼя класу батьківської моделі. Колонка imageable_type використовується Eloquent для визначення того, який «тип» батьківської моделі повернути під час звернення до звʼязку imageable. У цьому випадку колонка міститиме або App\Models\Post, або App\Models\User.
Структура моделей
Далі розгляньмо визначення моделей, потрібні для побудови цього звʼязку:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Image extends Model
{
/**
* Отримати батьківську модель imageable (користувача або допис).
*/
public function imageable(): MorphTo
{
return $this->morphTo();
}
}
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
class Post extends Model
{
/**
* Отримати зображення допису.
*/
public function image(): MorphOne
{
return $this->morphOne(Image::class, 'imageable');
}
}
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
class User extends Model
{
/**
* Отримати зображення користувача.
*/
public function image(): MorphOne
{
return $this->morphOne(Image::class, 'imageable');
}
}
Отримання звʼязку
Щойно вашу таблицю бази даних і моделі визначено, ви можете звертатися до звʼязків через ваші моделі. Наприклад, щоб отримати зображення для допису, ми можемо звернутися до динамічної властивості звʼязку image:
use App\Models\Post;
$post = Post::find(1);
$image = $post->image;
Ви можете отримати батьківську модель поліморфної моделі, звернувшись до імені методу, який виконує виклик morphTo. У цьому випадку це метод imageable у моделі Image. Отже, ми звернемося до цього методу як до динамічної властивості звʼязку:
use App\Models\Image;
$image = Image::find(1);
$imageable = $image->imageable;
Звʼязок imageable у моделі Image поверне або екземпляр Post, або User, залежно від того, який тип моделі володіє зображенням.
Домовленості щодо ключів
За потреби ви можете вказати імена колонок «id» та «type», які використовує ваша поліморфна дочірня модель. Якщо ви це робите, переконайтеся, що ви завжди передаєте імʼя звʼязку першим аргументом методу morphTo. Зазвичай це значення має збігатися з іменем методу, тож ви можете використати константу PHP __FUNCTION__:
/**
* Отримати модель, якій належить зображення.
*/
public function imageable(): MorphTo
{
return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');
}
Один до багатьох (поліморфний)
Структура таблиць
Поліморфний звʼязок «один до багатьох» схожий на звичайний «один до багатьох», але дочірня модель через одну асоціацію може належати до кількох типів моделей. Скажімо, користувачі вашого застосунку можуть залишати коментарі до постів і до відео. З поліморфними звʼязками одна таблиця comments міститиме коментарі і для постів, і для відео. Спершу подивімося на структуру таблиць, потрібну для такого звʼязку:
posts
id - integer
title - string
body - text
videos
id - integer
title - string
url - string
comments
id - integer
body - text
commentable_type - string
commentable_id - integer
Структура моделей
Далі розгляньмо визначення моделей, потрібні для цього звʼязку:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Comment extends Model
{
/**
* Отримати батьківську commentable-модель (пост або відео).
*/
public function commentable(): MorphTo
{
return $this->morphTo();
}
}
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Post extends Model
{
/**
* Отримати всі коментарі поста.
*/
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Video extends Model
{
/**
* Отримати всі коментарі відео.
*/
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
Отримання звʼязку
Коли таблиці й моделі визначені, до звʼязків можна звертатися через динамічні властивості моделі. Наприклад, щоб дістати всі коментарі поста, скористаємося динамічною властивістю comments:
use App\Models\Post;
$post = Post::find(1);
foreach ($post->comments as $comment) {
// ...
}
Батьківську модель поліморфного нащадка можна отримати через назву методу, який викликає morphTo. У цьому випадку це метод commentable моделі Comment. Звертаємося до нього як до динамічної властивості звʼязку:
use App\Models\Comment;
$comment = Comment::find(1);
$commentable = $comment->commentable;
Звʼязок commentable моделі Comment поверне екземпляр Post або Video - залежно від того, модель якого типу є батьківською для коментаря.
Автоматичне наповнення батьківських моделей у нащадках
Навіть із жадібним завантаженням Eloquent проблема «N + 1» може виникнути, якщо в циклі по дочірніх моделях звертатися до батьківської:
$posts = Post::with('comments')->get();
foreach ($posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->commentable->title;
}
}
У прикладі вище зʼявилася проблема «N + 1»: коментарі для кожної моделі Post завантажені жадібно, але Eloquent не наповнює автоматично батьківський Post у кожній дочірній моделі Comment.
Щоб Eloquent автоматично наповнював батьківські моделі в нащадках, викличте метод chaperone при визначенні звʼязку morphMany:
class Post extends Model
{
/**
* Отримати всі коментарі поста.
*/
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable')->chaperone();
}
}
Або, якщо автоматичне наповнення батьківських моделей потрібно вмикати під час виконання, викличте chaperone при жадібному завантаженні звʼязку:
use App\Models\Post;
$posts = Post::with([
'comments' => fn ($comments) => $comments->chaperone(),
])->get();
Один із багатьох (поліморфний)
Іноді модель має багато повʼязаних моделей, а потрібно зручно діставати «найновішу» чи «найстарішу» з них. Наприклад, модель User може бути повʼязана з багатьма моделями Image, і ви хочете зручно працювати з найсвіжішим завантаженим зображенням. Це робиться через тип звʼязку morphOne у поєднанні з методами ofMany:
/**
* Отримати найсвіжіше зображення користувача.
*/
public function latestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->latestOfMany();
}
Так само можна визначити метод для отримання «найстарішої», тобто першої, повʼязаної моделі:
/**
* Отримати найстаріше зображення користувача.
*/
public function oldestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->oldestOfMany();
}
За замовчуванням методи latestOfMany і oldestOfMany шукають найновішу або найстарішу повʼязану модель за первинним ключем моделі, який має бути сортовним. Але часом одну модель із більшого звʼязку треба дістати за іншим критерієм сортування.
Наприклад, методом ofMany можна отримати зображення користувача з найбільшою кількістю «лайків». Перший аргумент ofMany - колонка для сортування, другий - агрегатна функція (min або max), яку застосувати при пошуку повʼязаної моделі:
/**
* Отримати найпопулярніше зображення користувача.
*/
public function bestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');
}
[!NOTE] Можна будувати й складніші звʼязки «один із багатьох». Детальніше про це - у документації про has one of many.
Багато до багатьох (поліморфний)
Структура таблиць
Поліморфні звʼязки «багато до багатьох» трохи складніші за «morph one» і «morph many». Наприклад, моделі Post і Video можуть мати спільний поліморфний звʼязок із моделлю Tag. Поліморфний звʼязок «багато до багатьох» дозволяє тримати в застосунку одну таблицю унікальних тегів, які можна привʼязувати до постів або відео. Спершу подивімося на структуру таблиць для такого звʼязку:
posts
id - integer
name - string
videos
id - integer
name - string
tags
id - integer
name - string
taggables
tag_id - integer
taggable_type - string
taggable_id - integer
[!NOTE] Перш ніж занурюватись у поліморфні звʼязки «багато до багатьох», варто прочитати документацію про звичайні звʼязки «багато до багатьох».
Структура моделей
Тепер визначмо звʼязки в моделях. Моделі Post і Video матимуть метод tags, який викликає метод morphToMany базового класу моделі Eloquent.
Метод morphToMany приймає назву повʼязаної моделі та «назву звʼязку». За назвою проміжної таблиці та ключами, які вона містить, ми називатимемо цей звʼязок «taggable»:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
class Post extends Model
{
/**
* Отримати всі теги поста.
*/
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
}
Визначення зворотного боку звʼязку
Далі в моделі Tag треба визначити метод для кожної з можливих батьківських моделей. У нашому прикладі це методи posts і videos. Обидва мають повертати результат виклику morphedByMany.
Метод morphedByMany приймає назву повʼязаної моделі та «назву звʼязку». За назвою проміжної таблиці та ключами, які вона містить, ми називатимемо цей звʼязок «taggable»:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
class Tag extends Model
{
/**
* Отримати всі пости, яким призначено цей тег.
*/
public function posts(): MorphToMany
{
return $this->morphedByMany(Post::class, 'taggable');
}
/**
* Отримати всі відео, яким призначено цей тег.
*/
public function videos(): MorphToMany
{
return $this->morphedByMany(Video::class, 'taggable');
}
}
Отримання звʼязку
Коли таблиці й моделі визначені, до звʼязків можна звертатися через моделі. Наприклад, щоб дістати всі теги поста, скористайтеся динамічною властивістю звʼязку tags:
use App\Models\Post;
$post = Post::find(1);
foreach ($post->tags as $tag) {
// ...
}
Батьківську модель поліморфного звʼязку можна отримати з дочірньої моделі через назву методу, який викликає morphedByMany. У цьому випадку це методи posts або videos моделі Tag:
use App\Models\Tag;
$tag = Tag::find(1);
foreach ($tag->posts as $post) {
// ...
}
foreach ($tag->videos as $video) {
// ...
}
Власні поліморфні типи
За замовчуванням Laravel зберігає «тип» повʼязаної моделі як повне імʼя класу. Наприклад, у розглянутому вище звʼязку «один до багатьох», де модель Comment може належати Post або Video, значенням commentable_type за замовчуванням буде App\Models\Post або App\Models\Video відповідно. Але ці значення можна відвʼязати від внутрішньої структури застосунку.
Скажімо, замість імен моделей як «типу» використати прості рядки post і video. Тоді значення поліморфної колонки «type» у базі лишаться коректними навіть після перейменування моделей:
use Illuminate\Database\Eloquent\Relations\Relation;
Relation::enforceMorphMap([
'post' => 'App\Models\Post',
'video' => 'App\Models\Video',
]);
Викликати метод enforceMorphMap можна в методі boot класу App\Providers\AppServiceProvider або, за бажанням, створити для цього окремий сервіс-провайдер.
Морф-аліас конкретної моделі під час виконання можна визначити методом моделі getMorphClass. І навпаки, повне імʼя класу, повʼязане з морф-аліасом, дає метод Relation::getMorphedModel:
use Illuminate\Database\Eloquent\Relations\Relation;
$alias = $post->getMorphClass();
$class = Relation::getMorphedModel($alias);
[!WARNING] Коли ви додаєте «morph map» до наявного застосунку, кожне значення поліморфної колонки
*_typeу базі, яке досі містить повне імʼя класу, треба перетворити на відповідне імʼя з мапи.
Динамічні звʼязки
Метод resolveRelationUsing дозволяє визначати звʼязки між моделями Eloquent під час виконання. Для звичайної розробки застосунків це зазвичай не рекомендують, але при написанні пакетів для Laravel іноді буває корисно.
Перший аргумент resolveRelationUsing - потрібна назва звʼязку. Другий - замикання, яке приймає екземпляр моделі й повертає коректне визначення звʼязку Eloquent. Зазвичай динамічні звʼязки налаштовують у методі boot сервіс-провайдера:
use App\Models\Order;
use App\Models\Customer;
Order::resolveRelationUsing('customer', function (Order $orderModel) {
return $orderModel->belongsTo(Customer::class, 'customer_id');
});
[!WARNING] Визначаючи динамічні звʼязки, завжди передавайте методам звʼязків Eloquent явні аргументи з назвами ключів.
Запити до звʼязків
Оскільки всі звʼязки Eloquent визначаються методами, ці методи можна викликати, щоб отримати екземпляр звʼязку без виконання запиту на завантаження повʼязаних моделей. До того ж усі типи звʼязків Eloquent працюють і як конструктори запитів, тож до запиту звʼязку можна далі додавати обмеження ланцюжком, перш ніж виконати SQL проти бази.
Наприклад, уявіть блог, у якому модель User має багато повʼязаних моделей Post:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Model
{
/**
* Отримати всі пости користувача.
*/
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
}
Запит до звʼязку posts із додатковими обмеженнями виглядає так:
use App\Models\User;
$user = User::find(1);
$user->posts()->where('active', 1)->get();
На звʼязку доступні будь-які методи конструктора запитів Laravel, тож перегляньте його документацію, щоб знати про всі наявні можливості.
Ланцюжок orWhere після звʼязків
Як показано вище, до звʼязків при запитах можна додавати додаткові обмеження. Але з orWhere у ланцюжку будьте обережні: такі умови логічно згрупуються на одному рівні з обмеженням самого звʼязку:
$user->posts()
->where('active', 1)
->orWhere('votes', '>=', 100)
->get();
Приклад вище згенерує такий SQL. Як бачите, умова or змушує запит повертати будь-який пост із понад 100 голосами. Запит більше не обмежений конкретним користувачем:
select *
from posts
where user_id = ? and active = 1 or votes >= 100
У більшості випадків варто використовувати логічні групи, щоб згрупувати умови в дужках:
use Illuminate\Database\Eloquent\Builder;
$user->posts()
->where(function (Builder $query) {
return $query->where('active', 1)
->orWhere('votes', '>=', 100);
})
->get();
Цей приклад дасть такий SQL. Зверніть увагу: логічне групування правильно згрупувало обмеження, і запит лишається обмеженим конкретним користувачем:
select *
from posts
where user_id = ? and (active = 1 or votes >= 100)
Методи звʼязків проти динамічних властивостей
Якщо додаткові обмеження до запиту звʼязку не потрібні, до звʼязку можна звертатись як до властивості. Продовжуючи приклад із моделями User і Post, усі пости користувача дістаємо так:
use App\Models\User;
$user = User::find(1);
foreach ($user->posts as $post) {
// ...
}
Динамічні властивості звʼязків виконують «ліниве завантаження»: дані звʼязку завантажуються лише тоді, коли ви до них звертаєтесь. Через це розробники часто застосовують жадібне завантаження для звʼязків, до яких точно звертатимуться після завантаження моделі. Жадібне завантаження помітно зменшує кількість SQL-запитів, потрібних для завантаження звʼязків моделі.
Запит на наявність звʼязку
Отримуючи записи моделей, ви можете обмежити результат за наявністю звʼязку. Наприклад, потрібні всі пости блогу, які мають хоча б один коментар. Для цього передайте назву звʼязку методам has і orHas:
use App\Models\Post;
// Отримати всі пости, які мають хоча б один коментар...
$posts = Post::has('comments')->get();
Можна також вказати оператор і кількість, щоб точніше налаштувати запит:
// Отримати всі пости, які мають три або більше коментарів...
$posts = Post::has('comments', '>=', 3)->get();
Вкладені конструкції has будуються через «крапкову» нотацію. Наприклад, так можна дістати всі пости, що мають хоча б один коментар, який має хоча б одне зображення:
// Отримати пости, що мають хоча б один коментар із зображеннями...
$posts = Post::has('comments.images')->get();
Якщо потрібно більше можливостей, методи whereHas і orWhereHas дозволяють задати додаткові обмеження запиту в has-запитах, наприклад перевірити вміст коментаря:
use Illuminate\Database\Eloquent\Builder;
// Отримати пости, що мають хоча б один коментар зі словами на кшталт code%...
$posts = Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
})->get();
// Отримати пости, що мають щонайменше десять коментарів зі словами на кшталт code%...
$posts = Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
}, '>=', 10)->get();
[!WARNING] Наразі Eloquent не підтримує запитів на наявність звʼязку між різними базами даних. Звʼязки мають існувати в межах однієї бази.
Запити наявності звʼязку «багато до багатьох»
Метод whereAttachedTo дозволяє знаходити моделі, привʼязані звʼязком «багато до багатьох» до певної моделі або колекції моделей:
$users = User::whereAttachedTo($role)->get();
У whereAttachedTo можна передати й екземпляр колекції. Тоді Laravel отримає моделі, привʼязані до будь-якої з моделей у цій колекції:
$tags = Tag::whereLike('name', '%laravel%')->get();
$posts = Post::whereAttachedTo($tags)->get();
Вбудовані запити наявності звʼязку
Якщо наявність звʼязку треба перевірити з однією простою умовою where на запиті звʼязку, зручнішими будуть методи whereRelation, orWhereRelation, whereMorphRelation і orWhereMorphRelation. Наприклад, знайдімо всі пости з незатвердженими коментарями:
use App\Models\Post;
$posts = Post::whereRelation('comments', 'is_approved', false)->get();
Звісно, як і у виклику where конструктора запитів, можна вказати оператор:
$posts = Post::whereRelation(
'comments', 'created_at', '>=', now()->minus(hours: 1)
)->get();
Запит на відсутність звʼязку
Отримуючи записи моделей, ви можете обмежити результат за відсутністю звʼязку. Наприклад, потрібні всі пости блогу, які не мають жодного коментаря. Для цього передайте назву звʼязку методам doesntHave і orDoesntHave:
use App\Models\Post;
$posts = Post::doesntHave('comments')->get();
Якщо потрібно більше можливостей, методи whereDoesntHave і orWhereDoesntHave додають до doesntHave-запитів додаткові обмеження, наприклад перевірку вмісту коментаря:
use Illuminate\Database\Eloquent\Builder;
$posts = Post::whereDoesntHave('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
})->get();
«Крапкова» нотація дозволяє виконати запит до вкладеного звʼязку. Наприклад, наступний запит поверне всі пости без коментарів, а також пости, коментарі яких не належать жодному заблокованому користувачеві:
use Illuminate\Database\Eloquent\Builder;
$posts = Post::whereDoesntHave('comments.author', function (Builder $query) {
$query->where('banned', 1);
})->get();
Запити до звʼязків morph to
Щоб перевірити наявність звʼязків «morph to», використовуйте методи whereHasMorph і whereDoesntHaveMorph. Перший аргумент цих методів - назва звʼязку. Далі йдуть назви повʼязаних моделей, які ви хочете включити в запит. Наостанок можна передати замикання, яке налаштує запит звʼязку:
use App\Models\Comment;
use App\Models\Post;
use App\Models\Video;
use Illuminate\Database\Eloquent\Builder;
// Отримати коментарі, повʼязані з постами або відео, у яких title схожий на code%...
$comments = Comment::whereHasMorph(
'commentable',
[Post::class, Video::class],
function (Builder $query) {
$query->where('title', 'like', 'code%');
}
)->get();
// Отримати коментарі, повʼязані з постами, у яких title не схожий на code%...
$comments = Comment::whereDoesntHaveMorph(
'commentable',
Post::class,
function (Builder $query) {
$query->where('title', 'like', 'code%');
}
)->get();
Інколи обмеження запиту треба будувати залежно від «типу» повʼязаної поліморфної моделі. Замикання, передане в whereHasMorph, може приймати значення $type другим аргументом, що дозволяє перевірити «тип» запиту, який зараз будується:
use Illuminate\Database\Eloquent\Builder;
$comments = Comment::whereHasMorph(
'commentable',
[Post::class, Video::class],
function (Builder $query, string $type) {
$column = $type === Post::class ? 'content' : 'title';
$query->where($column, 'like', 'code%');
}
)->get();
Іноді потрібно шукати нащадків батьківської моделі звʼязку «morph to». Для цього є методи whereMorphedTo і whereNotMorphedTo, які самі визначають правильне зіставлення морф-типу для переданої моделі. Перший аргумент - назва звʼязку morphTo, другий - повʼязана батьківська модель:
$comments = Comment::whereMorphedTo('commentable', $post)
->orWhereMorphedTo('commentable', $video)
->get();
Запит до всіх повʼязаних моделей
Замість масиву можливих поліморфних моделей можна передати * як значення-шаблон. Тоді Laravel дістане з бази всі можливі поліморфні типи, виконавши для цього додатковий запит:
use Illuminate\Database\Eloquent\Builder;
$comments = Comment::whereHasMorph('commentable', '*', function (Builder $query) {
$query->where('title', 'like', 'foo%');
})->get();
Агрегація повʼязаних моделей
Підрахунок повʼязаних моделей
Іноді потрібно порахувати кількість повʼязаних моделей для звʼязку, не завантажуючи самі моделі. Для цього є метод withCount, який додає до отриманих моделей атрибут {relation}_count:
use App\Models\Post;
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count;
}
Передавши в withCount масив, можна додати «кількості» для кількох звʼязків, а також додаткові обмеження до запитів:
use Illuminate\Database\Eloquent\Builder;
$posts = Post::withCount(['votes', 'comments' => function (Builder $query) {
$query->where('content', 'like', 'code%');
}])->get();
echo $posts[0]->votes_count;
echo $posts[0]->comments_count;
Результату підрахунку можна дати аліас, що дозволяє мати кілька підрахунків по одному звʼязку:
use Illuminate\Database\Eloquent\Builder;
$posts = Post::withCount([
'comments',
'comments as pending_comments_count' => function (Builder $query) {
$query->where('approved', false);
},
])->get();
echo $posts[0]->comments_count;
echo $posts[0]->pending_comments_count;
Відкладене завантаження кількості
Метод loadCount дозволяє завантажити кількість для звʼязку вже після того, як батьківську модель отримано:
$book = Book::first();
$book->loadCount('genres');
Якщо запиту на підрахунок потрібні додаткові обмеження, передайте масив із ключами - назвами звʼязків, які ви хочете порахувати. Значення масиву мають бути замиканнями, які отримують екземпляр конструктора запитів:
$book->loadCount(['reviews' => function (Builder $query) {
$query->where('rating', 5);
}])
Підрахунок звʼязків і власні select-вирази
Якщо ви поєднуєте withCount із виразом select, викликайте withCount після методу select:
$posts = Post::select(['title', 'body'])
->withCount('comments')
->get();
Інші агрегатні функції
Крім withCount, Eloquent пропонує методи withMin, withMax, withAvg, withSum і withExists. Вони додають до отриманих моделей атрибут {relation}_{function}_{column}:
use App\Models\Post;
$posts = Post::withSum('comments', 'votes')->get();
foreach ($posts as $post) {
echo $post->comments_sum_votes;
}
Якщо до результату агрегатної функції хочеться звертатися під іншою назвою, вкажіть власний аліас:
$posts = Post::withSum('comments as total_comments', 'votes')->get();
foreach ($posts as $post) {
echo $post->total_comments;
}
Як і для loadCount, для цих методів є відкладені версії. Такі додаткові агрегатні операції можна виконувати на вже отриманих моделях Eloquent:
$post = Post::first();
$post->loadSum('comments', 'votes');
Якщо ви поєднуєте ці агрегатні методи з виразом select, викликайте їх після методу select:
$posts = Post::select(['title', 'body'])
->withExists('comments')
->get();
Підрахунок повʼязаних моделей у звʼязках morph to
Щоб жадібно завантажити звʼязок «morph to» разом із кількостями повʼязаних моделей для різних сутностей, які цей звʼязок може повернути, скористайтеся методом with у поєднанні з методом morphWithCount звʼязку morphTo.
Припустімо, що моделі Photo і Post можуть створювати моделі ActivityFeed. Модель ActivityFeed визначає звʼязок «morph to» на імʼя parentable, який дозволяє отримати батьківську модель Photo або Post для конкретного екземпляра ActivityFeed. Крім того, моделі Photo «мають багато» моделей Tag, а моделі Post - багато моделей Comment.
Тепер уявімо, що потрібно отримати екземпляри ActivityFeed і жадібно завантажити для кожного з них батьківські моделі parentable. Додатково потрібна кількість тегів кожного батьківського фото і кількість коментарів кожного батьківського поста:
use Illuminate\Database\Eloquent\Relations\MorphTo;
$activities = ActivityFeed::with([
'parentable' => function (MorphTo $morphTo) {
$morphTo->morphWithCount([
Photo::class => ['tags'],
Post::class => ['comments'],
]);
}])->get();
Відкладене завантаження кількості
Припустімо, набір моделей ActivityFeed уже отримано, і тепер треба завантажити вкладені кількості для різних моделей parentable, повʼязаних із цією стрічкою активності. Для цього є метод loadMorphCount:
$activities = ActivityFeed::with('parentable')->get();
$activities->loadMorphCount('parentable', [
Photo::class => ['tags'],
Post::class => ['comments'],
]);
Жадібне завантаження
Коли ви звертаєтесь до звʼязків Eloquent як до властивостей, повʼязані моделі завантажуються ліниво: дані звʼязку не читаються з бази, доки ви вперше не звернетесь до властивості. Але Eloquent уміє «жадібно завантажувати» звʼязки одночасно із запитом до батьківської моделі. Жадібне завантаження знімає проблему «N + 1». Щоб її показати, розгляньмо модель Book, яка «належить» моделі Author:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Book extends Model
{
/**
* Отримати автора, який написав книгу.
*/
public function author(): BelongsTo
{
return $this->belongsTo(Author::class);
}
}
Тепер отримаймо всі книги та їхніх авторів:
use App\Models\Book;
$books = Book::all();
foreach ($books as $book) {
echo $book->author->name;
}
Цей цикл виконає один запит, щоб дістати всі книги з таблиці, а потім ще по одному запиту на кожну книгу, щоб отримати її автора. Тобто для 25 книг код вище виконає 26 запитів: один на книги і ще 25 на авторів кожної книги.
На щастя, жадібним завантаженням цю операцію можна звести до двох запитів. Будуючи запит, вкажіть методом with, які звʼязки треба завантажити жадібно:
$books = Book::with('author')->get();
foreach ($books as $book) {
echo $book->author->name;
}
Для цієї операції виконаються лише два запити - один дістане всі книги, другий - усіх авторів для цих книг:
select * from books
select * from authors where id in (1, 2, 3, 4, 5, ...)
Жадібне завантаження кількох звʼязків
Іноді треба жадібно завантажити кілька різних звʼязків. Просто передайте методу with масив звʼязків:
$books = Book::with(['author', 'publisher'])->get();
Вкладене жадібне завантаження
Щоб жадібно завантажити звʼязки звʼязку, використовуйте «крапковий» синтаксис. Наприклад, завантажмо всіх авторів книги та всі особисті контакти автора:
$books = Book::with('author.contacts')->get();
Як варіант, вкладені звʼязки для жадібного завантаження можна задати вкладеним масивом у методі with - це зручно, коли вкладених звʼязків кілька:
$books = Book::with([
'author' => [
'contacts',
'publisher',
],
])->get();
Вкладене жадібне завантаження звʼязків morphTo
Щоб жадібно завантажити звʼязок morphTo разом із вкладеними звʼязками різних сутностей, які він може повернути, використайте метод with у поєднанні з методом morphWith звʼязку morphTo. Для прикладу розгляньмо таку модель:
<?php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class ActivityFeed extends Model
{
/**
* Отримати батьківський запис для стрічки активності.
*/
public function parentable(): MorphTo
{
return $this->morphTo();
}
}
Припустімо, що моделі Event, Photo і Post можуть створювати моделі ActivityFeed. Крім того, моделі Event належать моделі Calendar, моделі Photo повʼязані з моделями Tag, а моделі Post належать моделі Author.
З такими визначеннями моделей і звʼязків можна отримати екземпляри ActivityFeed і жадібно завантажити всі моделі parentable та їхні вкладені звʼязки:
use Illuminate\Database\Eloquent\Relations\MorphTo;
$activities = ActivityFeed::query()
->with(['parentable' => function (MorphTo $morphTo) {
$morphTo->morphWith([
Event::class => ['calendar'],
Photo::class => ['tags'],
Post::class => ['author'],
]);
}])->get();
Жадібне завантаження окремих колонок
Не завжди потрібні всі колонки звʼязків, які ви отримуєте. Тому Eloquent дозволяє вказати, які саме колонки звʼязку діставати:
$books = Book::with('author:id,name,book_id')->get();
[!WARNING] Користуючись цією можливістю, завжди включайте в список потрібних колонок
idі всі відповідні колонки зовнішніх ключів.
Жадібне завантаження за замовчуванням
Іноді певні звʼязки треба завантажувати завжди, коли ви отримуєте модель. Для цього визначте в моделі властивість $with:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Book extends Model
{
/**
* Звʼязки, які завжди мають завантажуватись.
*
* @var array
*/
protected $with = ['author'];
/**
* Отримати автора, який написав книгу.
*/
public function author(): BelongsTo
{
return $this->belongsTo(Author::class);
}
/**
* Отримати жанр книги.
*/
public function genre(): BelongsTo
{
return $this->belongsTo(Genre::class);
}
}
Щоб прибрати елемент із властивості $with для одного запиту, скористайтеся методом without:
$books = Book::without('author')->get();
Щоб для одного запиту перевизначити всі елементи властивості $with, скористайтеся методом withOnly:
$books = Book::withOnly('genre')->get();
Обмеження жадібного завантаження
Іноді звʼязок треба завантажити жадібно, але з додатковими умовами запиту. Для цього передайте методу with масив звʼязків, де ключ - назва звʼязку, а значення - замикання, яке додає обмеження до запиту жадібного завантаження:
use App\Models\User;
$users = User::with(['posts' => function ($query) {
$query->where('title', 'like', '%code%');
}])->get();
У цьому прикладі Eloquent жадібно завантажить лише ті пости, у колонці title яких є слово code. Щоб додатково налаштувати жадібне завантаження, можна викликати й інші методи конструктора запитів:
$users = User::with(['posts' => function ($query) {
$query->orderBy('created_at', 'desc');
}])->get();
Обмеження жадібного завантаження звʼязків morphTo
Під час жадібного завантаження звʼязку morphTo Eloquent виконує кілька запитів - по одному на кожен тип повʼязаної моделі. Додаткові обмеження до кожного з цих запитів додає метод constrain звʼязку MorphTo:
use Illuminate\Database\Eloquent\Relations\MorphTo;
$comments = Comment::with(['commentable' => function (MorphTo $morphTo) {
$morphTo->constrain([
Post::class => function ($query) {
$query->whereNull('hidden_at');
},
Video::class => function ($query) {
$query->where('type', 'educational');
},
]);
}])->get();
У цьому прикладі Eloquent жадібно завантажить лише неприховані пости і відео зі значенням type, рівним "educational".
Обмеження жадібного завантаження за наявністю звʼязку
Часом треба одночасно перевірити наявність звʼязку і завантажити його за тими самими умовами. Наприклад, вам потрібні лише моделі User, які мають дочірні моделі Post, що відповідають певній умові, і разом із ними жадібно завантажені відповідні пости. Це робить метод withWhereHas:
use App\Models\User;
$users = User::withWhereHas('posts', function ($query) {
$query->where('featured', true);
})->get();
Ліниве жадібне завантаження
Іноді звʼязок треба завантажити жадібно вже після того, як батьківську модель отримано. Це стає в пригоді, коли рішення про завантаження повʼязаних моделей ухвалюється динамічно:
use App\Models\Book;
$books = Book::all();
if ($condition) {
$books->load('author', 'publisher');
}
Якщо запиту жадібного завантаження потрібні додаткові обмеження, передайте масив із ключами - назвами звʼязків, які треба завантажити. Значення масиву мають бути замиканнями, які отримують екземпляр запиту:
$author->load(['books' => function ($query) {
$query->orderBy('published_date', 'asc');
}]);
Щоб завантажити звʼязок лише тоді, коли він ще не завантажений, використовуйте метод loadMissing:
$book->loadMissing('author');
Вкладене ліниве жадібне завантаження і morphTo
Щоб жадібно завантажити звʼязок morphTo разом із вкладеними звʼязками різних сутностей, які він може повернути, скористайтеся методом loadMorph.
Перший аргумент цього методу - назва звʼязку morphTo, другий - масив пар «модель / звʼязки». Для прикладу розгляньмо таку модель:
<?php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class ActivityFeed extends Model
{
/**
* Отримати батьківський запис для стрічки активності.
*/
public function parentable(): MorphTo
{
return $this->morphTo();
}
}
Припустімо, що моделі Event, Photo і Post можуть створювати моделі ActivityFeed. Крім того, моделі Event належать моделі Calendar, моделі Photo повʼязані з моделями Tag, а моделі Post належать моделі Author.
З такими визначеннями моделей і звʼязків можна отримати екземпляри ActivityFeed і жадібно завантажити всі моделі parentable та їхні вкладені звʼязки:
$activities = ActivityFeed::with('parentable')
->get()
->loadMorph('parentable', [
Event::class => ['calendar'],
Photo::class => ['tags'],
Post::class => ['author'],
]);
Автоматичне жадібне завантаження
У багатьох випадках Laravel може жадібно завантажувати звʼязки, до яких ви звертаєтесь, автоматично. Щоб увімкнути автоматичне жадібне завантаження, викличте метод Model::automaticallyEagerLoadRelationships у методі boot класу AppServiceProvider вашого застосунку:
use Illuminate\Database\Eloquent\Model;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Model::automaticallyEagerLoadRelationships();
}
Коли цю можливість увімкнено, Laravel намагатиметься автоматично завантажити будь-які звʼязки, до яких ви звертаєтесь і які ще не були завантажені. Розгляньмо такий сценарій:
use App\Models\User;
$users = User::all();
foreach ($users as $user) {
foreach ($user->posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->content;
}
}
}
Зазвичай код вище виконав би запит для кожного користувача, щоб отримати його пости, і ще запит для кожного поста, щоб отримати його коментарі. Але з увімкненою можливістю automaticallyEagerLoadRelationships Laravel сам виконає ліниве жадібне завантаження постів для всіх користувачів у колекції, щойно ви звернетеся до постів будь-кого з отриманих користувачів. Так само при зверненні до коментарів будь-якого отриманого поста коментарі будуть ліниво жадібно завантажені для всіх спочатку отриманих постів.
Якщо вмикати автоматичне жадібне завантаження глобально не хочеться, його можна увімкнути для окремого екземпляра колекції Eloquent, викликавши на колекції метод withRelationshipAutoloading:
$users = User::where('vip', true)->get();
return $users->withRelationshipAutoloading();
Заборона лінивого завантаження
Як уже сказано, жадібне завантаження звʼязків часто дає застосунку відчутний виграш у продуктивності. Тож за бажанням ви можете наказати Laravel завжди забороняти ліниве завантаження звʼязків. Для цього викличте метод preventLazyLoading базового класу моделі Eloquent, зазвичай у методі boot класу AppServiceProvider вашого застосунку.
Метод preventLazyLoading приймає необовʼязковий булевий аргумент, який вказує, чи забороняти ліниве завантаження. Наприклад, ви можете вимикати ліниве завантаження лише в непродакшн-середовищах, щоб продакшн і далі працював нормально, навіть якщо в коді випадково лишиться ліниво завантажуваний звʼязок:
use Illuminate\Database\Eloquent\Model;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}
Після заборони лінивого завантаження Eloquent кидатиме виняток Illuminate\Database\LazyLoadingViolationException, коли застосунок спробує ліниво завантажити будь-який звʼязок Eloquent.
Поведінку при порушеннях правила лінивого завантаження можна налаштувати методом handleLazyLoadingViolationsUsing. Наприклад, так можна лише логувати порушення, не перериваючи виконання застосунку винятками:
Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {
$class = $model::class;
info("Attempted to lazy load [{$relation}] on model [{$class}].");
});
Вставка й оновлення повʼязаних моделей
Метод save
Eloquent має зручні методи для додавання нових моделей у звʼязки. Скажімо, треба додати новий коментар до поста. Замість того щоб вручну виставляти атрибут post_id моделі Comment, вставте коментар методом save самого звʼязку:
use App\Models\Comment;
use App\Models\Post;
$comment = new Comment(['message' => 'A new comment.']);
$post = Post::find(1);
$post->comments()->save($comment);
Зверніть увагу: ми зверталися не до динамічної властивості comments, а викликали метод comments, щоб отримати екземпляр звʼязку. Метод save сам додасть новій моделі Comment правильне значення post_id.
Якщо треба зберегти кілька повʼязаних моделей, скористайтеся методом saveMany:
$post = Post::find(1);
$post->comments()->saveMany([
new Comment(['message' => 'A new comment.']),
new Comment(['message' => 'Another new comment.']),
]);
Методи save і saveMany збережуть передані екземпляри моделей, але не додадуть щойно збережені моделі до вже завантажених у памʼять звʼязків батьківської моделі. Якщо ви плануєте звертатися до звʼязку після виклику save чи saveMany, варто перезавантажити модель та її звʼязки методом refresh:
$post->comments()->save($comment);
$post->refresh();
// Усі коментарі, включно зі щойно збереженим...
$post->comments;
Рекурсивне збереження моделей і звʼязків
Якщо потрібно зберегти (save) модель разом з усіма повʼязаними з нею звʼязками, скористайтеся методом push. У цьому прикладі буде збережено модель Post, а також її коментарі та авторів коментарів:
$post = Post::find(1);
$post->comments[0]->message = 'Message';
$post->comments[0]->author->name = 'Author Name';
$post->push();
Метод pushQuietly зберігає модель разом з її звʼязками, не викликаючи жодних подій:
$post->pushQuietly();
Метод create
Окрім методів save і saveMany, доступний метод create, який приймає масив атрибутів, створює модель і вставляє її в базу даних. Різниця між save і create у тому, що save приймає повноцінний екземпляр моделі Eloquent, а create - звичайний PHP-масив. Новостворену модель метод create поверне як результат:
use App\Models\Post;
$post = Post::find(1);
$comment = $post->comments()->create([
'message' => 'A new comment.',
]);
Щоб створити кілька повʼязаних моделей, скористайтеся методом createMany:
$post = Post::find(1);
$post->comments()->createMany([
['message' => 'A new comment.'],
['message' => 'Another new comment.'],
]);
Методи createQuietly і createManyQuietly створюють модель (чи моделі) без відправлення жодних подій:
$user = User::find(1);
$user->posts()->createQuietly([
'title' => 'Post title.',
]);
$user->posts()->createManyQuietly([
['title' => 'First post.'],
['title' => 'Second post.'],
]);
Також ви можете використовувати методи findOrNew, firstOrNew, firstOrCreate і updateOrCreate, щоб створювати й оновлювати моделі у звʼязках.
[!NOTE] Перед використанням методу
createперегляньте документацію про масове призначення.
Звʼязки Belongs To
Якщо потрібно призначити дочірню модель новій батьківській, скористайтеся методом associate. У цьому прикладі модель User визначає звʼязок belongsTo з моделлю Account. Метод associate встановить зовнішній ключ на дочірній моделі:
use App\Models\Account;
$account = Account::find(10);
$user->account()->associate($account);
$user->save();
Щоб відʼєднати батьківську модель від дочірньої, використайте метод dissociate. Він встановить зовнішньому ключу звʼязку значення null:
$user->account()->dissociate();
$user->save();
Звʼязки «багато до багатьох»
Приєднання / відʼєднання
Eloquent надає методи, які роблять роботу зі звʼязками «багато до багатьох» зручнішою. Припустімо, користувач може мати багато ролей, а роль - багато користувачів. Метод attach приєднає роль до користувача, вставивши запис у проміжну таблицю звʼязку:
use App\Models\User;
$user = User::find(1);
$user->roles()->attach($roleId);
Приєднуючи звʼязок до моделі, ви також можете передати масив додаткових даних для вставки у проміжну таблицю:
$user->roles()->attach($roleId, ['expires' => $expires]);
Інколи роль треба забрати в користувача. Щоб видалити запис звʼязку «багато до багатьох», використайте метод detach. Він видалить відповідний запис із проміжної таблиці, але обидві моделі залишаться в базі даних:
// Відʼєднати одну роль від користувача...
$user->roles()->detach($roleId);
// Відʼєднати всі ролі від користувача...
$user->roles()->detach();
Для зручності attach і detach приймають також масиви ідентифікаторів:
$user = User::find(1);
$user->roles()->detach([1, 2, 3]);
$user->roles()->attach([
1 => ['expires' => $expires],
2 => ['expires' => $expires],
]);
Синхронізація звʼязків
Для побудови звʼязків «багато до багатьох» також підходить метод sync. Він приймає масив ідентифікаторів, які мають лишитися у проміжній таблиці. Усі ідентифікатори, яких немає в переданому масиві, буде видалено з проміжної таблиці. Тобто після завершення операції у проміжній таблиці будуть тільки ті ідентифікатори, що були в масиві:
$user->roles()->sync([1, 2, 3]);
Разом з ідентифікаторами можна передати додаткові значення для проміжної таблиці:
$user->roles()->sync([1 => ['expires' => true], 2, 3]);
Якщо потрібно вставити однакові значення проміжної таблиці для кожного із синхронізованих ідентифікаторів, використайте метод syncWithPivotValues:
$user->roles()->syncWithPivotValues([1, 2, 3], ['active' => true]);
Коли відʼєднувати наявні ідентифікатори, яких немає в переданому масиві, не потрібно, скористайтеся методом syncWithoutDetaching:
$user->roles()->syncWithoutDetaching([1, 2, 3]);
Перемикання звʼязків
Звʼязок «багато до багатьох» також надає метод toggle, який «перемикає» стан приєднання для переданих ідентифікаторів повʼязаних моделей. Якщо ідентифікатор наразі приєднаний, його буде відʼєднано. І навпаки: якщо відʼєднаний, його буде приєднано:
$user->roles()->toggle([1, 2, 3]);
Разом з ідентифікаторами можна передати додаткові значення для проміжної таблиці:
$user->roles()->toggle([
1 => ['expires' => true],
2 => ['expires' => true],
]);
Транзакційні операції з проміжною таблицею
Кожна з описаних вище операцій з проміжною таблицею має варіант OrFail (attachOrFail, detachOrFail, syncOrFail, syncWithoutDetachingOrFail і toggleOrFail), який загортає операцію в транзакцію бази даних, тож усі зміни автоматично відкочуються, якщо буде викинуто виняток:
$user->roles()->attachOrFail([1, 2, 3]);
$user->roles()->syncOrFail([1, 2, 3]);
Оновлення запису у проміжній таблиці
Якщо потрібно оновити наявний рядок у проміжній таблиці звʼязку, використайте метод updateExistingPivot. Він приймає зовнішній ключ проміжного запису й масив атрибутів для оновлення:
$user = User::find(1);
$user->roles()->updateExistingPivot($roleId, [
'active' => false,
]);
Оновлення часових міток батьківської моделі
Коли модель визначає звʼязок belongsTo або belongsToMany з іншою моделлю - наприклад, Comment, що належить Post, - буває корисно оновлювати часову мітку батьківської моделі під час оновлення дочірньої.
Скажімо, після оновлення моделі Comment ви хочете автоматично «торкнутися» мітки updated_at у Post, якому належить коментар, щоб вона отримала поточні дату й час. Для цього застосуйте атрибут Touches до дочірньої моделі, вказавши в ньому назви звʼязків, чиї мітки updated_at мають оновлюватися разом з дочірньою моделлю:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Touches;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Touches(['post'])]
class Comment extends Model
{
/**
* Отримати пост, якому належить коментар.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}
[!WARNING] Часові мітки батьківської моделі оновляться лише тоді, коли дочірню модель збережено методом
saveEloquent.
Перекладаємо з офіційної документації, розділ за розділом, і не ховаємо недоперекладене. Помітили неточність у терміні чи реченні: напишіть, виправимо.