Звʼязки — Laravel
Частину підрозділів ще не перекладено — вони показані англійською нижче в тексті або лишились в оригіналі. Готові фрагменти вже перевірені редактором.
- Вступ
- Визначення звʼязків
- Звʼязки з обмеженнями (scoped)
- Звʼязки «багато до багатьох»
- Поліморфні звʼязки
- Динамічні звʼязки
- Запити до звʼязків
- Агрегація повʼязаних моделей
- Жадібне завантаження
- Вставка та оновлення повʼязаних моделей
- Оновлення часових міток батьківської моделі
Вступ
Таблиці бази даних часто повʼязані одна з одною. Наприклад, допис у блозі може мати багато коментарів, а замовлення може бути повʼязане з користувачем, який його зробив. 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');
}
Один до багатьох (поліморфний)
Структура таблиць
One to Many (Polymorphic) — Table Structure, Model Structure, Retrieving the Relationship, Key Conventions; One of Many (Polymorphic); Many to Many (Polymorphic) — Table Structure, Model Structure, Retrieving the Relationship, Retrieving All of the Models That Own a Tag; Custom Polymorphic Types; Dynamic Relationships; Querying Relations — Relationship Methods vs. Dynamic Properties, Querying Relationship Existence, Querying Relationship Absence, Querying Morph To Relationships; Aggregating Related Models — Counting Related Models, Other Aggregate Functions, Counting Related Models on Morph To Relationships; Eager Loading — Constraining Eager Loads, Lazy Eager Loading, Automatic Eager Loading, Preventing Lazy Loading; Inserting and Updating Related Models — The save Method, The create Method, Belongs To Relationships, Many to Many Relationships; Touching Parent Timestamps.
Виправити терміни, дописати розділ або взяти нову главу може кожен. Термінологію узгоджуємо в глосарії, щоб переклад лишався однорідним.