Связи
Содержание:
Введение
Таблицы базы данных часто связаны одна с другой. Например, заметка в блоге может иметь
большое количество комментариев или заказ может быть связан с пользователем, который разместил его.
Eloquent делает управление и работу с этими отношеними простой. Поддерживаются следующие
различные типы отношений:
Определение отношений
Отношения Eloquent определены как методы для ваших Eloquent модели классов. Поэтому. как и сами
модели Eloquent, отношения также служат в качестве мощного
конструктора запросов, определяя отношения
как методы, что открывает большие возможности для составления запросов и использовании цепочки методов.
Например, вы можете добавить дополнительные ограничения цепочкой для отношения posts
:
$user->posts()->where('active', 1)->get();
Но перед тем как погрузиться глубже в использование отношений,
давайте научимся определять каждый тип.
Один-к-одному
Отношение один-к-одному — это самое базовое отношение. Например, модель User
(пользователь)
может быть связана с одним Phone
(телефоном). Чтобы определить эти отношения, мы разместим
метод phone
для модели User
. Метод phone
должен вызвать метод
hasOne
и вернуть его результаты:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Get the phone record associated with the user.
*/
public function phone()
{
return $this->hasOne('App\Phone');
}
}
Первый аргумент, передаваемый в метод hasOne
, является именем связанной модели.
После определения связи, мы можем получить связанную связь используя динимические свойства
Eloquent. Динамически свойства позволяют вам получить доступ к методом отношения так,
словно они были свойствами, определёнными для модели:
$phone = User::find(1)->phone;
Eloquent определяет внешний ключ отношения основанный на имени модели. В этом случае,
предполагается , что модель Phone
автоматически имеет внушний ключ user_id
.
Если вы хотите переопределить это соглашение, вы можете передать второй аргумент в метод
hasOne
:
return $this->hasOne('App\Phone', 'foreign_key');
Дополнительно, Eloquent предполагает, что внешние ключи должны иметь значение, которое соответвует
id
(или пользовательское $primaryKey
) столбцу родителя. Другими словами,
Eloquent будет искать значение столбца пользователя id
в столбце user_id
для записи Phone
. Если вы хотите, чтобы отношение использовало значение отличное от
id
, вы можете передать третий аргумент в метод hasOne
указывающий
ваш пользовательский ключ:
return $this->hasOne('App\Phone', 'foreign_key', 'local_key');
Определения для второй модели.
Теперь, мы можем получить доступ к модели Phone
из User
. Теперь, давайте
определим отношение для модели Phone
, которое позволит нам получить доступ к
пользователю User
, который владеет телефоном. Мы можем определить обратное hasOne
отношение используя метод belongsTo
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Phone extends Model
{
/**
* Get the user that owns the phone.
*/
public function user()
{
return $this->belongsTo('App\User');
}
}
В примере выше, Eloquent будет пытаться соединить user_id
из модели Phone
с id
для модели User
. Eloquent определяет имя внешнего ключа путём
добавления _id
к имени метода. Однако, если внешний ключ для модели Phone
не user_id
, вы можете передать собственное имя в качестве второго аргумента в метод
belongsTo
:
/**
* Get the user that owns the phone.
*/
public function user()
{
return $this->belongsTo('App\User', 'foreign_key');
}
Если ваша родительская модель не использует id
в качестве первичного ключа, или вы хотели
присоединить дочернюю модель к другому столбцу, вы можете передать третий аргумент в метод
belongsTo
, указывая пользовательский ключ вашей родительской таблицы:
/**
* Get the user that owns the phone.
*/
public function user()
{
return $this->belongsTo('App\User', 'foreign_key', 'other_key');
}
Один-ко-многим
Отношение один ко многим используется для определения отношений,
когда одиночная модель владеет любым количеством других моделей.
Например, заметка блога может иметь много комментариев. Как и все другие отношения Eloquent,
отношения один ко многим определяется путём размещения функции для вашей модели Eloquent:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* Get the comments for the blog post.
*/
public function comments()
{
return $this->hasMany('App\Comment');
}
}
Запомните, Eloquent будет автоматически определять нужную колонку внешнего ключа для модели Comment
.
По соглашению, Eloquent возьмёт "змеиный_регистр" имени модели и добавит суффикс _id
.
Поэтому, для этого примера, Eloquent будет автоматически считать post_id
внешним ключом
для модели Comment
.
После определения отношения, мы можем получить доступ к коллекции комментариев через свойство
comments
. В силу того, что Eloquent предоставляет динамические свойства,
мы можем использовать методы отношений так, словно они были определены как свойства для модели:
$comments = App\Post::find(1)->comments;
foreach ($comments as $comment) {
//
}
В силу того, что отношения выступают в качестве составной части конструктора запросов,
вы можете добавить другие ограничения для получаемых комментариев путём вызова метода
comments
и продолжения цепочки условий для запроса:
$comment = App\Post::find(1)->comments()->where('title', 'foo')->first();
Как и для метода hasOne
, вы можете также переопределить внешние и локальные ключи
путём передачи дополнительных аргументов в метод hasMany
:
return $this->hasMany('App\Comment', 'foreign_key');
return $this->hasMany('App\Comment', 'foreign_key', 'local_key');
Один-ко-многим (инверсия)
Теперь мы можем получить доступ ко всем комментарием заметки. Давайте определим отношение,
чтобы получать доступ через комментарий к родительской заметке. Для определения инверсии
отношения hasMany
, определим функцию отношение для дочерней модели,
которая вызывает метод belongsTo
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
/**
* Get the post that owns the comment.
*/
public function post()
{
return $this->belongsTo('App\Post');
}
}
После определения отношения, мы можем получить модель Post
для
Comment
путём доступа к динамическому свойству post
:
$comment = App\Comment::find(1);
echo $comment->post->title;
В примере выше, Eloquent будет пытаться соединить post_id
из модели Comment
с id
для модели Post
. Eloquent определяет имя базового внешнего ключа
путём проверки имени метода отношений и добовления суффикса с _
и именем колонки
первичного ключа. Если внешний ключ для модели Comment
не post_id
,
вы можете передать пользовательское имя ключа в качестве второго аргумента в метод belongsTo
:
/**
* Get the post that owns the comment.
*/
public function post()
{
return $this->belongsTo('App\Post', 'foreign_key');
}
Если родительская модель не использует id
в качестве первичного ключа, или вы хотите
присоединить дочернюю модель к другому столбцу, вы можете передать третий аргумент в метод
code>belongsTo, указывая ваш пользовательский ключ для таблицы родителя:
/**
* Get the post that owns the comment.
*/
public function post()
{
return $this->belongsTo('App\Post', 'foreign_key', 'other_key');
}
Многие-ко-многим
Связь "многие-ко-многим" чуть более сложная чем связь hasOne
и hasMany
.
Примером такой связи может быть пользователь с большим числом ролей, где роли доступны для
других пользователей. Например, много пользователей могут иметь роль "Admin".
Для определения этих связей, необходимы три таблицы базы данных: users
, roles
, и
role_user
. Таблица role_user
выводится из имён связанных моделей
в алфавитном порядке и содержит столбцы user_id
и role_id
columns.
Связь многие-ко-многим определяется путём написания метода, который вернёт результаты метода
belongsToMany
. Например, давайте определим метод roles
для нашей
модели User
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The roles that belong to the user.
*/
public function roles()
{
return $this->belongsToMany('App\Role');
}
}
После определения связи, вы можете получить роли пользователя используя динамическое свойство
roles
:
$user = App\User::find(1);
foreach ($user->roles as $role) {
//
}
Как и для других типов связей, вы можете вызвать метод roles
, чтобы продолжить цепочку
ограничений запроса для отношения:
$roles = App\User::find(1)->roles()->orderBy('name')->get();
Как упоминалось ранее, для определения имени таблицы, которая выступает в качестве объединённой таблицы связей,
Eloquent присоединит два связанных имени модели в алфавитном порядке. Но вы можете переопределить такое
соглашения. Вы можете сделать так путём передачи второго аргумента в метод belongsToMany
:
return $this->belongsToMany('App\Role', 'role_user');
Дополнительно к настройке имени объединённой таблицы, вы можете тажке настроить имена ключей столбцов
для таблицы путём передачи дополнительных аргументов в метод belongsToMany
.
Третий аргумент является именем внешнего ключа модели для которой определено отношение,
четвёртый аргумент является именем внешнего ключа модели, которую вы присоединяете:
return $this->belongsToMany('App\Role', 'role_user', 'user_id', 'role_id');
Определения обратной версии связи
Для определения обратного отношения для связи многие-ко-многим, вам необходимо разместить другой вызов
belongsToMany
для связанной модели. Продолжая пример с пользователями и ролями,
давайте определим метод users
для модели Role
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
/**
* The users that belong to the role.
*/
public function users()
{
return $this->belongsToMany('App\User');
}
}
Как вы можете видеть, отношение определено таким же образом, как и для User
,
с различием в том, что ссылаемся на модель App\User
. В силу того, что мы повторно
используем метод belongsToMany
, все обычные таблицы и ключевые опции кастомизации
доступны при определении обратной версии связи многие-ко-многим.
Получение промежуточных столбцов таблицы
Как вы уже поняли, работа с отношениями многие-ко-многим требуют присутсвия дополнительной таблицы.
Eloquent предоставляет несколько полезных способов взаимодействия с этой таблицей. Например, давайте
предположим, что наш объект User
располагает большим количествоам объектов Role
,
с которыми они и свзяны. После получения доступа к этому отношению, мы можем получить доступ
к промежуточной таблице используя атрибут pivot
для моделей:
$user = App\User::find(1);
foreach ($user->roles as $role) {
echo $role->pivot->created_at;
}
Заметьте, что для каждой модели Role
, которую мы получаем, автоматически приписывается
атрибут pivot
. Этот атрибут содержит модель, представляющую промежуточную таблицу,
и можно использовать как любую другую модель Eloquent.
По умолчанию, только ключи модели будут присутсвовать в объекте pivot
. Если сводная таблица
содержит дополнительные атрибуты, вы должны указать их при использовании этой связи:
return $this->belongsToMany('App\Role')->withPivot('column1', 'column2');
Если вы хотите чтобы ваша сводная таблица поддерживала метки времени created_at
и
updated_at
, используйте метод withTimestamps
для определения связи:
return $this->belongsToMany('App\Role')->withTimestamps();
Настройка сводных атрибутов имени
Как упоминалось ранее, атрибуты из дополнительной таблицы могут быть получены в модели используя
атрибут pivot
. Вы можете настроить имя этого атрибута, чтобы лучше показать его цели
внутри вашего приложения.
Например, если ваше приложение содержит пользователей, которые могут подписать на подкасты,
то вы скорее всего имеете связь многие-ко-многим между пользователями и подкастами. В этом случае,
вы можете захотеть переименовать средство доступа для вашей дополнительной таблицы на subscription
вместо pivot
. Это можно сделать использую метод as
при определении отношения:
return $this->belongsToMany('App\Podcast')
->as('subscription')
->withTimestamps();
После того как это сделано, вы можете получить доступ к данным дополнительной таблицы используя
пользовательское имя:
$users = User::with('podcasts')->get();
foreach ($users->flatMap->podcasts as $podcast) {
echo $podcast->subscription->created_at;
}
Фильтрация связей через столбцы промежуточной таблицы
Вы можете также отфильтровать результаты возвращаемые belongsToMany
используя
методы wherePivot
и wherePivotIn
при определении связи:
return $this->belongsToMany('App\Role')->wherePivot('approved', 1);
return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);
Если вы хотите определить пользовательскую модель для представления дополнительной таблицы
ваших связей, вы можете вызвать метод using
при определении связи.
Пользовательские многие-ко-многим сводные модели должны расширять класс
Illuminate\Database\Eloquent\Relations\Pivot
, в то время как пользовательские
полиморфные модели многие-ко-многим должны расширять класс
Illuminate\Database\Eloquent\Relations\MorphPivot
. Например, мы можем определить
класс Role
, который использует пользовательску сводную модель RoleUser
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
/**
* The users that belong to the role.
*/
public function users()
{
return $this->belongsToMany('App\User')->using('App\RoleUser');
}
}
При определении модели RoleUser
, мы будем расширять класс Pivot
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Relations\Pivot;
class RoleUser extends Pivot
{
//
}
Вы можете скомбинировать using
и withPivot
, чтобы получить столбцы из
дополнительной таблицы. Например, вы можете получить столбцы created_by
и
updated_by
из сводной таблицы RoleUser
путём передачи имен столбцов
в метод withPivot
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
/**
* The users that belong to the role.
*/
public function users()
{
return $this->belongsToMany('App\User')
->using('App\RoleUser')
->withPivot([
'created_by',
'updated_by',
]);
}
}
Сводные модели могут не использовать трейт мягкого удаления SoftDeletes
.
Если вам необходимо мягко удалить записи сводной таблицы, вам необходимо преобразовать
вашу модель в актуальную Eloquent модель.
Пользовательские сводные модели и инкрементные идентификаторы
Если вы определили связь много-ко-многим, которая использует пользовательскую сводну модель,
и у этой сводной модели есть автоинкрементный первичный ключ, вы должны убедиться, что класс
вашей сводной таблицы определил свойство incrementing
, значение которого
установлено как true
.
/**
* Indicates if the IDs are auto-incrementing.
*
* @var bool
*/
public $incrementing = true;
К-одному-через
Связь "к-одному-через" связывает модели через промежуточное отношение. Например, если у каждого
поставщика есть один пользователь, и каждый пользователь ассоциирован с одной записью истории пользователя,
тогда каждая модель поставщика может получить доступ к истории пользователя через самого пользователя.
Давайте взглянем на таблицы базы данных, необходимых для определения этого отношения:
users
id - integer
supplier_id - integer
suppliers
id - integer
history
id - integer
user_id - integer
Хотя таблица history
не содержит столбца supplier_id
,
связь hasOneThrough
может обеспечить доступ к истории пользоватяля через модель поставщика.
Теперь, после того, как мы изучили структуру таблицы для отношения, давайте определим связь для модели
Supplier
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Supplier extends Model
{
/**
* Get the user's history.
*/
public function userHistory()
{
return $this->hasOneThrough('App\History', 'App\User');
}
}
Первый аргумент, передаваемый в метод hasOneThrough
является именем конечной модели,
доступ к которой мы хотим получить. Второй аргумент является именем промежуточной модели.
Типовое соглашение Eloquent для внешних ключей будет использовано при реализации запросов связи.
Если вы хотите настроить ваши ключи для связи, вы можете передавать их как третий и четвёртый аргументы
в метод hasOneThrough
. Третий аргумент является именем внешнего ключа для средней модели.
Четвёртый аргумент является именем внешнего ключа для конечной модели. Кроме того, доступны пятый
и шестой аргументы. Пятый аргумент является локальным ключом, а шестой будет локальным ключом для
промежуточной модели:
class Supplier extends Model
{
/**
* Get the user's history.
*/
public function userHistory()
{
return $this->hasOneThrough(
'App\History',
'App\User',
'supplier_id', // Foreign key on users table...
'user_id', // Foreign key on history table...
'id', // Local key on suppliers table...
'id' // Local key on users table...
);
}
}
Ко-многим-через
Отношение "Ко-многим-через" обечпечивает удобный короткий путь для доступа к удалённым
связям через промежуточное отношение. Например, модель Country
может иметь большое
количество моделей Post
через промежуточную модель User
. В этом примере,
вы можете легко собрать все заметки для данной страны. Давайте взглянем на таблицы,
необходимые для определения этого отношения:
countries
id - integer
name - string
users
id - integer
country_id - integer
name - string
posts
id - integer
user_id - integer
title - string
Хотя posts
не содержит столбец country_id
, связь hasManyThrough
обеспечивает доступ к записям страны через через $country->posts
.
Для реализации такого запроса, Eloquent проверяет country_id
для промежуточной
таблицы users
. После нахождения соответсвующих ID пользователя, они используются
для запроса к таблице posts
.
Теперь, после изучения структуры таблицы для отношения, давайте определим связь для модели
Country
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Country extends Model
{
/**
* Get all of the posts for the country.
*/
public function posts()
{
return $this->hasManyThrough('App\Post', 'App\User');
}
}
Первый аргумент, передаваемый в метод hasManyThrough
является именем конечной модели,
к которой мы хотели бы получить доступ. Второй аргумент является именем промежуточной модели.
Типовое соглашение Eloquent для внешних ключей будет использовано для реализации запросов связи.
Чтобы настроить ваши пользовательские ключи, вы можете передать из в качестве третьего и четвёртого
аргумента в метод hasManyThrough
. Третий аргумент является именем для внешнего ключа
для промежуточной модели. Четрвёрты аргумент является именем внешнего ключа для конечной модели.
Пятый аргумент является локальным ключом, а шестой аргумент будет локальным ключом для промежуточной
модели:
class Country extends Model
{
public function posts()
{
return $this->hasManyThrough(
'App\Post',
'App\User',
'country_id', // Foreign key on users table...
'user_id', // Foreign key on posts table...
'id', // Local key on countries table...
'id' // Local key on users table...
);
}
}
Полиморфные отношения
Полиморфное отношение позволяют целевой модели принадлежать к нескольким типам связей
используя одну ассоциацию.
Полиморфное отношение один-к-одному
Структура таблицы
Один-к-одному полиморфная связь похожа на обыкновенное отношение один-к-одному; однакое, целевая модель
может принадлежать к более чем одному типу модели в одной ассоциации. Например, запись блога
Post
и пользователь User
могут разделить полиморфное отношение
с моделью Image
. Используя один-к-одному полиморфную связь позволит вам получить
список уникальных изображений, которые используются и для заметок блога, и для аккаунтов пользователей.
Во-первых, давайте изучим структуру таблицы:
posts
id - integer
name - string
users
id - integer
name - string
images
id - integer
url - string
imageable_id - integer
imageable_type - string
Нужно обратить внимание на столбцы imageable_id
и imageable_type
для таблицы images
. Столбец imageable_id
будет содержать идентификатор
заметки или пользователя, в то время как столбец imageable_type
будет содержать имя класса
родительской модели. Столбец imageable_type
используется Eloquent для определения какой "тип"
родительской модели необходимо возвращать при обращении к связи imageable
.
Структура модели
После чего, проверим определение модели, необходимое для построения этой связи:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Image extends Model
{
/**
* Get the owning imageable model.
*/
public function imageable()
{
return $this->morphTo();
}
}
class Post extends Model
{
/**
* Get the post's image.
*/
public function image()
{
return $this->morphOne('App\Image', 'imageable');
}
}
class User extends Model
{
/**
* Get the user's image.
*/
public function image()
{
return $this->morphOne('App\Image', 'imageable');
}
}
Получение связи
После определения таблиц базы данных и моделей, вы можете получать доступ к связям через ваши модели.
Например, чтобы получить изображение для записи, мы можем использовать динамическое свойство
image
:
$post = App\Post::find(1);
$image = $post->image;
Вы также можете получить родителя через полиморфную модель путём обращения к имени метода,
выполняющего вызов MorphTo
. В нашем случае, это метод imageable
для модели Image
. Итак, мы будем обращаться к этому методу как к динамическому свойству:
$image = App\Image::find(1);
$imageable = $image->imageable;
Связь imageable
для модели Image
будет возвращать экземпляр
Post
или User
в зависимости от того, какой тип модели владеет изображением.
Полиморфное отношение один-ко-многим
Структура таблицы
Полиморфная связь один-ко-многим похожа на простуя свяь один-ко-многим. Однако, целевая модель
может принадлежать к более чем одному типу модели в одной ассоциации. Например, представьте,
что пользователи могут комментировать и заметки, и видео. Используя полиморфные связи, вы можете
использовать одиночную таблицу comments
для обоих этих сценариев. Во-первых,
давайте определим структуру таблицы, необходимую для построения этого отношения:
posts
id - integer
title - string
body - text
videos
id - integer
title - string
url - string
comments
id - integer
body - text
commentable_id - integer
commentable_type - string
Структура модели
После чего, давайте обратимся к определению модели, необходимому для построения этого отношения:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
/**
* Get the owning commentable model.
*/
public function commentable()
{
return $this->morphTo();
}
}
class Post extends Model
{
/**
* Get all of the post's comments.
*/
public function comments()
{
return $this->morphMany('App\Comment', 'commentable');
}
}
class Video extends Model
{
/**
* Get all of the video's comments.
*/
public function comments()
{
return $this->morphMany('App\Comment', 'commentable');
}
}
Получение отношения
После определения таблиц базы данных и моделей, вы можете получать связи через модели.
Например, чтобы получить все комментарии для заметки, мы можем использовать динамическое
свойство comments
:
$post = App\Post::find(1);
foreach ($post->comments as $comment) {
//
}
Вы можете получить владельца полиморфного отношения из модели обратившись к имя метода,
выполняющего вызов MorphTo
. В нашем случае, это метод commentable
для модели Comment
. Поэтому, мы будем обращаться к этому методу
как к динамическому свойству:
$comment = App\Comment::find(1);
$commentable = $comment->commentable;
Связь commentable
для модели Comment
вернёт или
экземпляр Post
или Video
, в зависимости от типа модели, владеющего комментарием.
Полиморфное отношение много-ко-многим
Структура таблицы
Полиморфное отношение много-ко-многим чуть более сложное, чем отношения morphOne
и
morphMany
. Например, модель заметки блога Post
или code>Video
могут разделить полиморфное отношение с моделью Tag
. Полиморфное отношение
много-ко-многим позволяет вам иметь единый список уникальных тэгов, которые используются и заметках блога,
и для видео контента. Первое, давайте проверим структуру таблицы:
posts
id - integer
name - string
videos
id - integer
name - string
tags
id - integer
name - string
taggables
tag_id - integer
taggable_id - integer
taggable_type - string
Структура модели
Следующим шагом мы готовы определить связь для модели. Обе модели Post
и Video
будут иметь метод tags
, который вызывает метод morphToMany
в базовом классе
Eloquent:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* Get all of the tags for the post.
*/
public function tags()
{
return $this->morphToMany('App\Tag', 'taggable');
}
}
Определение второй части связи
После, для модели Tag
вы должны определить метод для каждой из связанных моделей.
Поэтому, для этого примера, мы определим метод posts
и метод videos
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
/**
* Get all of the posts that are assigned this tag.
*/
public function posts()
{
return $this->morphedByMany('App\Post', 'taggable');
}
/**
* Get all of the videos that are assigned this tag.
*/
public function videos()
{
return $this->morphedByMany('App\Video', 'taggable');
}
}
Получение связи
После определения ваших таблиц базы данных и моделей, вы можете воспользоваться связью через
ваши модели. Например, чтобы получить все теги заметки, вы можете использовать
динамическое свойство tags
:
$post = App\Post::find(1);
foreach ($post->tags as $tag) {
//
}
Вы можете также получить владельца полиморфной связи из полиморфной модели обратившись к имени
метода, выполняющего вызов morphedByMany
. В нашем случае, это методы
posts
и videos
для модели Tag
. Итак, вы сможете использовать
эти методы как динамические свойства:
$tag = App\Tag::find(1);
foreach ($tag->videos as $video) {
//
}
Пользовательские полиморфные типы
По умолчанию, Ларавел будет использовать полное имя класса для хранения типа связанной модели.
Например, данная связь один-к-многим, указанная выше, где Comment
может принадлежать
к Post
или Video
, базовое commentable_type
должно быть
App\Post
или App\Video
, соответсвенно. Однако, вы можете захотеть
отделить вашу базу данных от внутренней структуры приложения. В этом случае, вы можете определить
карту "morph map", чтобы научить Eloquent использовать пользовательское имя для каждой
модели вместо имени класса:
use Illuminate\Database\Eloquent\Relations\Relation;
Relation::morphMap([
'posts' => 'App\Post',
'videos' => 'App\Video',
]);
Вы можете регистрировать morphMap
в функции boot
вашего
AppServiceProvider
или создать отдельный сервис провайдер.
При добавлении "morph map" в ваше существующее приложение,
вам также необходимо изменить значение столбца в базе данных, которое всё ещё
содержит полное имя класса.
Запросы отношений
В силу того, что все типы отношений Eloquent определены через методы, вы можете вызвать эти методы,
чтобы получить экземпляр связ без фактического выполнения запросов связи. Дополнительно, все типы
отношений Eloquent обслуживаются как конструктор запросов, что позволяет задавать дополнитеьные условия
цепочкой для связи до момента фактического выполнения SQL для ваше базы данных.
Например, представим систему блога, в которой модель пользователя User
имеет
много ассоциаций с моделями заметок Post
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Get all of the posts for the user.
*/
public function posts()
{
return $this->hasMany('App\Post');
}
}
Вы можете обратиться к связи заметки posts
и добавить дополнитлеьные ограничения
для связи следующим образом:
$user = App\User::find(1);
$user->posts()->where('active', 1)->get();
Вы можете использовать любые методы конструктора запросов для связи. Поэтому, рекомендуется
обратиться к соответсвующему разделу документации.
Добавление условий orWhere
после обращения к связи
Как показано в примере выше, вы можете легко добавлять дополнительные ограничения к связи,
при выполнении запроса. Однако, использовать условия orWhere
для связи нужно
осторожно, так как orWhere
условия будут логически сгруппированы на том же уровне,
что и ограничения связи:
$user->posts()
->where('active', 1)
->orWhere('votes', '>=', 100)
->get();
// 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();
// select * from posts
// where user_id = ? and (active = 1 or votes >= 100)
Методы отношений и динамические свойства
Если вам не нужно вводить дополнительных ограничений в запрос связи Eloquent, вы можете получить доступ
к отнешению так, словно это свойство. Например, продолжая линию с примером моделей пользователя User
и заметки Post
, мы можем поулчить все заметки пользователя таким образом:
$user = App\User::find(1);
foreach ($user->posts as $post) {
//
}
Для динамических свойств действует правило "ленивой загрузки", это означает, что данные связи будут
подгружаться только при фактческом обращении к ним. Поэтому, разработчики часто используют "безотложную загрузку",
чтобы предварительно загрузить отношение, с которым они будут иметь дело. "Безотложная загрузка" может
значительно сократить количество SQL запросов, загружаемых для работы со связями моделей.
Запрос существования отношений
При запросе записи для модели, вы можете наложить ограничения на результаты основываясь на
существовании связи. Например, давайте предствим, что вы хотите получить все записи блога,
у которых есть как миниму один комментарий. Чтобы это сделать, вы можете передать имя связи
в методы has
или orHas
:
// Retrieve all posts that have at least one comment...
$posts = App\Post::has('comments')->get();
Вы также можете указать оператор и число для дальнейшей настройки запроса:
// Retrieve all posts that have three or more comments...
$posts = App\Post::has('comments', '>=', 3)->get();
Вложенные операторы has
могут быть также построены используя пунктуацию с точками.
Например, вы можете получить все заметки, в которых есть один комментарий с голосами:
// Retrieve posts that have at least one comment with votes...
$posts = App\Post::has('comments.votes')->get();
Если вам нужны ещё более можные решения, вы можете использовать методы whereHas
и orWhereHas
, чтобы разместить условия "where" для ваших запросов has
.
Эти методы позволяют вам добавлять дополнительные пользовательские ограничение к ограничениям связи,
таким как проверка контента комментария:
use Illuminate\Database\Eloquent\Builder;
// Retrieve posts with at least one comment containing words like foo%...
$posts = App\Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'foo%');
})->get();
// Retrieve posts with at least ten comments containing words like foo%...
$posts = App\Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'foo%');
}, '>=', 10)->get();
Запрос отсутствия отношений
При получении записей для модели, вы можете захотеть ограничить ваши результаты, основываясь
на отсутсвии связи. Например, представим, что мы хотим получить все записи блога, у которых нет
комментариев. Чтобы сделать так, вы можете передать имя связи в методы doesntHave
и orDoesntHave
:
$posts = App\Post::doesntHave('comments')->get();
Если вам нужно еще больше возможностей, вы можете использовать методы whereDoesntHave
и orWhereDoesntHave
, чтобы наложить условия для ваших запросов doesntHave
.
Эти методы позволят вам добавлять дополнительные ограничения для связи, такие как проврека контента
комментария:
use Illuminate\Database\Eloquent\Builder;
$posts = App\Post::whereDoesntHave('comments', function (Builder $query) {
$query->where('content', 'like', 'foo%');
})->get();
Вы можете использовать пунктуацию с точками для вложенных связей. Например, следующий запрос
получит все заметки с комментариями от авторов, которые не в бане:
use Illuminate\Database\Eloquent\Builder;
$posts = App\Post::whereDoesntHave('comments.author', function (Builder $query) {
$query->where('banned', 1);
})->get();
Запрос полиморфных отношений
Чтобы сделать запрос существования связи MorphTo
, вы можете использовать метод
whereHasMorph
и соответсвующие ему методы:
use Illuminate\Database\Eloquent\Builder;
// Retrieve comments associated to posts or videos with a title like foo%...
$comments = App\Comment::whereHasMorph(
'commentable',
['App\Post', 'App\Video'],
function (Builder $query) {
$query->where('title', 'like', 'foo%');
}
)->get();
// Retrieve comments associated to posts with a title not like foo%...
$comments = App\Comment::whereDoesntHaveMorph(
'commentable',
'App\Post',
function (Builder $query) {
$query->where('title', 'like', 'foo%');
}
)->get();
Вы можете использовать параметр $type
, чтобы добавить различные ограничения в зависимости
от связанной модели:
use Illuminate\Database\Eloquent\Builder;
$comments = App\Comment::whereHasMorph(
'commentable',
['App\Post', 'App\Video'],
function (Builder $query, $type) {
$query->where('title', 'like', 'foo%');
if ($type === 'App\Post') {
$query->orWhere('content', 'like', 'foo%');
}
}
)->get();
Вместо передачи массива возможных полиморфных моделей, вы можете указать *
в качестве
маски и позволить Laravel получить все возможные полиморфные типы из базы данных. Ларавел выполнит
дополнительный запрос для обеспечения этой операции:
use Illuminate\Database\Eloquent\Builder;
$comments = App\Comment::whereHasMorph('commentable', '*', function (Builder $query) {
$query->where('title', 'like', 'foo%');
})->get();
Если вы хотите посчитать число результ из связи без реальной загрузки, вы можете использовать
метод withCount
. Этот метод добавит столбец {relation}_count
в конечный
результат выборки модели. Например:
$posts = App\Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count;
}
Вы можете добавить "counts" для нескольких связей,
также как и добавить различные ограничения в запросы:
use Illuminate\Database\Eloquent\Builder;
$posts = App\Post::withCount(['votes', 'comments' => function (Builder $query) {
$query->where('content', 'like', 'foo%');
}])->get();
echo $posts[0]->votes_count;
echo $posts[0]->comments_count;
Для результата подсчёта результатов вы можете придумать псевдоним,
позволяя проводить несколько подсчётов для одной и той же связи:
use Illuminate\Database\Eloquent\Builder;
$posts = App\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;
Если вы используете комбинацию выражений withCount
и select
, убедитесь,
что вызвали withCount
после метода select
:
$posts = App\Post::select(['title', 'body'])->withCount('comments')->get();
echo $posts[0]->title;
echo $posts[0]->body;
echo $posts[0]->comments_count;
Дополнительно, используя метод loadCount
, вы можете загружать подсчёт связи
после фактического получения родительской модели:
$book = App\Book::first();
$book->loadCount('genres');
Если вам необходимо установить дополнительные ограничения для отложенной загрузки,
вы можете передать массив с ключами отношений, которые необходимо загружать.
Значения массива должны быть экземплярами замыкания, которое получит экземплря конструктора запросов:
$book->loadCount(['reviews' => function ($query) {
$query->where('rating', 5);
}])
Отложенная загрузка
При обращении к связям Eloquent как свойствам, для данных используется "ленивая загрузка".
Это значит, что данные связи фактически не подгружаются до момента первого достпупа свойства.
Однако, Eloquent может использовать "безотложную загрузку", в момент запроса родительской модели.
"Безотложная загрузка" может помочь в решении N + 1 проблемы. Для иллюстрации такой проблемы,
давайте предстваим, что модель книги Book
связана с моделью автора Author
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Book extends Model
{
/**
* Get the author that wrote the book.
*/
public function author()
{
return $this->belongsTo('App\Author');
}
}
Теперь, давайте получим все книги и их авторов:
$books = App\Book::all();
foreach ($books as $book) {
echo $book->author->name;
}
Такой перебор будет выполнять 1 запрос для получения всех книг, и запрос для получение автора текущей книги.
Поэтому, если у нас 25 книг, такой перебор сгенерирует 26 запросов: 1 для получения книг
и еще 25 дополнительных для получения автора конкретной книги.
Однако, мы можем воспользоваться безотложной загрузкой и снизить количество операций до 2-ух.
При составлении запроса, вы можете указать какие связи необходимо использовать при помощи
метода with
:
$books = App\Book::with('author')->get();
foreach ($books as $book) {
echo $book->author->name;
}
Для этой операции будут выполнены только 2 запроса:
select * from books
select * from authors where id in (1, 2, 3, 4, 5, ...)
Безотложная загурзка нескольких связей
Иногда вы можете захотеть безотложно загрузить несколько различных связей одной операцией.
Чтобы это сделать, вы можете передать дополнительные аргументы в метод with
:
$books = App\Book::with(['author', 'publisher'])->get();
Вложенная безотложная загрузка
Чтобы безотложно загрузить вложенную связь, вы можете использовать синтаксис с точками. Например,
давайте загрузим все книги автора и все персональные контакты автора в одном выражении Eloquent:
$books = App\Book::with('author.contacts')->get();
Вложенная безотложная загрузка связи morphTo
Если хотите воспользоваться безотложной загрузкой для связи morphTo
, вы можете использовать
метод morphWith
. Вы можете комбинировать этот метод с with
.
Чтобы показать метод morphWith
давайте обратимся к следующей модели:
<?php
use Illuminate\Database\Eloquent\Model;
class ActivityFeed extends Model
{
/**
* Get the parent of the activity feed record.
*/
public function parentable()
{
return $this->morphTo();
}
}
В этом примере давайте представим, что модели Event
, Photo
и Post
могут создавать модели ActivityFeed
. Давайте также представим,
что модели событие Event
принадлежат модели календарь Calendar
,
модели заметок Post
принадлежат модели автора Author
. Также модели
фото Photo
ассоциированы с моделями тэгов Tag
.
Используя такое отношение моделей и связей, мы можем получить экземпляр модели 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 = App\Book::with('author:id,name')->get();
При использовании данной возможности, вы должны всегда включать столбец id
и любые другие столбцы с внешним ключом в список желаемых для получения столбцов.
Безотложная загрузка по умолчанию
Иногда вы можете захотеть всегда загружать некоторые связи при получении модели. Чтобы справиться с такой
задачей, вы можете определить свойство $with
для модели:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Book extends Model
{
/**
* The relationships that should always be loaded.
*
* @var array
*/
protected $with = ['author'];
/**
* Get the author that wrote the book.
*/
public function author()
{
return $this->belongsTo('App\Author');
}
}
Если вы хотите убрать элемент из свойства $with
для запроса, вы можете использовать
метод without
:
$books = App\Book::without('author')->get();
Ограничения отложенных загрузок
Иногда вы можете захотеть не только безотложно загрузить связь, но также указать дополнитлеьные
условия для запроса. Приведём пример:
$users = App\User::with(['posts' => function ($query) {
$query->where('title', 'like', '%first%');
}])->get();
В этом примере, Eloquent будет безотложно загружать заметки, где столбец заметки с заголовком
title
содержит слово first
. Вы можете вызывать другие методы конструктора
запросов для дальнейшей настройки:
$users = App\User::with(['posts' => function ($query) {
$query->orderBy('created_at', 'desc');
}])->get();
Методы конструктора запросов limit
и take
не следует использовать
при построении запросов с безотложной загрузкой.
Ленивая отложенная загрузка
Иногда может потребоваться безотложно загрузить связь после того, как родительская модель уже
была получена. Например, это может быть полезным, для принятия динамических решений о том,
когда загружать связанные модели:
$books = App\Book::all();
if ($someCondition) {
$books->load('author', 'publisher');
}
Если вам необходимо установить дополнительные ограничения при построении запроса безотложной загрузки,
вы можете передать массив с ключами связи, которую вы хотите загрузить. Значения массива должны быть
экземплярами Замыкания
, которое получает экзмепляр запроса:
$books->load(['author' => function ($query) {
$query->orderBy('published_date', 'asc');
}]);
Для загрузки связи только в том случае, когда она не была уже загружена,
используйте метод loadMissing
:
public function format(Book $book)
{
$book->loadMissing('author');
return [
'name' => $book->name,
'author' => $book->author->name,
];
}
Вложенная ленивая безотложная загрузка & morphTo
Если вы хотите безотложно загружить связь morphTo
, также как и вложенные связи
в различных сущностях, которые могут быть возвращены этой связью,
вы можете использовать метод loadMorph
.
Этот метод принимает имя связи morphTo
в качестве первого аргумента, и массив пар
модель / связь в качестве второго. Чтобы помочь проиллюстрировать этот метод, давайте обративмя
к следующей модели:
<?php
use Illuminate\Database\Eloquent\Model;
class ActivityFeed extends Model
{
/**
* Get the parent of the activity feed record.
*/
public function parentable()
{
return $this->morphTo();
}
}
В этом примере, давайте предположим, что модели code>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'],
]);
Вставка и обновление связанных моделей
Метод 'Save'
Eloquent предоставляет удобные методы добавления новых моделей в связи. Например,
возможно вам необходимо вставить запись о новом комментарии Comment
для модели заметки Post
. Вместо того, чтобы вручную устанавливать атрибут
post_id
для Comment
, вы можете вставить комментарий Comment
напрямую из метода связи save
:
$comment = new App\Comment(['message' => 'A new comment.']);
$post = App\Post::find(1);
$post->comments()->save($comment);
Надо заметить, что мы не возпользовались связью comments
как динамическим свойством.
Вместо этого, мы вызвали метод comments
, чтобы получить экземпляр отношения.
Метод save
автоматически добавляем соотвествующее значение post_id
в новую модель Comment
.
Если вам необходимо сохранить несколько связанных моделей, вы можете использовать
метод saveMany
:
$post = App\Post::find(1);
$post->comments()->saveMany([
new App\Comment(['message' => 'A new comment.']),
new App\Comment(['message' => 'Another comment.']),
]);
Рекурсивное сохранение моделей и отношений
Если вы хотите сохранить save
вашу модель и все ассоциированные связи, вы можете
использовать метод push
:
$post = App\Post::find(1);
$post->comments[0]->message = 'Message';
$post->comments[0]->author->name = 'Author Name';
$post->push();
Метод 'Create'
Дополнительно к методам save
и saveMany
вы также можете использовать
метод create
, который принимает массив аттрибутов, создает модели и вставляет их
в базу данных. Опять же, различие между save
и create
заключается в том,
что save
принимает весь экземпляр модели Eloquent, в то время как create
принимает типовой массив PHP:
$post = App\Post::find(1);
$comment = $post->comments()->create([
'message' => 'A new comment.',
]);
Перед использованием метода
create
, убедитесь в том, что просмотрели документацию
про
массовое назначение.
You may use the createMany
method to create multiple related models:
$post = App\Post::find(1);
$post->comments()->createMany([
[
'message' => 'A new comment.',
],
[
'message' => 'Another new comment.',
],
]);
Вы также можете использовать методы findOrNew
, firstOrNew
,
firstOrCreate
и updateOrCreate
для создания и обновления моделей для связи.
Обновление отношения "Принадлежит к"
При обнавлении связи belongsTo
, вы можете использовать метод associate
.
Этот метод установит внешний ключ для дочерней модели:
$account = App\Account::find(10);
$user->account()->associate($account);
$user->save();
При удалении связи belongsTo
, вы можете использовать метод dissociate
.
Этот метод установит внешний ключ связи на null
:
$user->account()->dissociate();
$user->save();
Базовые модели
Связи belongsTo
, hasOne
, hasOneThrough
, и morphOne
позволяют вам определить базовую модель, которая будет возвращена, если данная связь null
.
Этот паттерн описывается как
Null object (шаблон проектирования) и может помочь убрать проверку условий в коде.
В следующем примере, связь user
вернёт пустую модель App\User
,
если пользователей user
не прикреплено к заметке:
/**
* Get the author of the post.
*/
public function user()
{
return $this->belongsTo('App\User')->withDefault();
}
Чтобы наполнить базовую модель атрибутами, вы может передать массив или Замыкание
в метод withDefault
:
/**
* Get the author of the post.
*/
public function user()
{
return $this->belongsTo('App\User')->withDefault([
'name' => 'Guest Author',
]);
}
/**
* Get the author of the post.
*/
public function user()
{
return $this->belongsTo('App\User')->withDefault(function ($user, $post) {
$user->name = 'Guest Author';
});
}
Обновление отношения "Многие ко многим"
Прикрепление / Открепление
Eloquent также предоставляет несколько дополнительных методов-помощников для более удобной работы
со связанными моделями. Например, давайте представим, что пользователь может иметь много ролей и
у роли может быть много пользователей. Чтобы добавить роль пользователю,
путём вставки записи в промежуточную таблицу
используйте метод attach
:
$user = App\User::find(1);
$user->roles()->attach($roleId);
При добавлении связи для модели, вы можете также передать массив дополнительных данных
для вставки в промежуточную таблицу:
$user->roles()->attach($roleId, ['expires' => $expires]);
Иногда может быть необхдимо убрать роль от пользователя. Чтобы убрать запись для отношения "многие ко многим",
используйте метод detach
. Метод detach
удалит соответсвующую запись
из промежуточной таблицы, а обе модели останутся в базе данных:
// Detach a single role from the user...
$user->roles()->detach($roleId);
// Detach all roles from the user...
$user->roles()->detach();
Для удобства, attach
и detach
также принимают массив ID
в качестве входных данных:
$user = App\User::find(1);
$user->roles()->detach([1, 2, 3]);
$user->roles()->attach([
1 => ['expires' => $expires],
2 => ['expires' => $expires],
]);
Синхронизация связей
Вы можете использовать метод sync
для построения "многие ко многим" ассоциаций.
Метод sync
принимает массив ID для размещения в промежуточной таблице.
Любые идентификаторы, которые не в данном массиве будут убраны из промежуточной таблицы.
Поэтому, после завершения этой операции, только указанные в массиве ID будут
существовать в промежуточной таблице:
$user->roles()->sync([1, 2, 3]);
Вы можете также передать дополнительные табличные значения с идентификаторами:
$user->roles()->sync([1 => ['expires' => true], 2, 3]);
Если вы не хотите убирать существующие ID, вы можете использовать метод syncWithoutDetaching
:
$user->roles()->syncWithoutDetaching([1, 2, 3]);
Переключение ассоциаций
Связь "многие ко многим" также предоставляет метод toggle
, который переключает
статус для данных ID. Если данный ID на данный момент прикреплён, его открепят.
В противоположном случае, если сейчас откреплён, то будет прикреплён:
$user->roles()->toggle([1, 2, 3]);
Сохранение дополнительных данных в сводной таблице
При работые со связью "многие ко многим", метод save
принимает массив необязатлеьных
промежуточных данных таблицы в качестве второго аргумента:
App\User::find(1)->roles()->save($role, ['expires' => $expires]);
Обновление записи в сводной таблице
Если вам необходимо обновить существующую строку в сводной таблице, вы можете использовать
метод updateExistingPivot
. Этот метод принимает внешний ключ сводной записи
и массив атрибутов для обновления:
$user = App\User::find(1);
$user->roles()->updateExistingPivot($roleId, $attributes);
Привязка родительских временных полей
Может быть полезно обновить родительские метки времени при обновлении дочерней модели.
Такие возможности можно использовать когда модель принадлежит или принадлежит ко многим
моделиям. Например, комментарии Comment
принадлежат заметкам Post
.
При обновлении модели комментариев Comment
, вы хотите автоматически обновить
метки времени для соответсвующей заметки Post
. Eloquent позволяет легко справиться
с такой задачей. Просто добавьте свойство touches
содержащее имена связей
с дочерней моделью:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
/**
* All of the relationships to be touched.
*
* @var array
*/
protected $touches = ['post'];
/**
* Get the post that the comment belongs to.
*/
public function post()
{
return $this->belongsTo('App\Post');
}
}
Теперь, при обновлении комментария Comment
, старшая модель заметки Post
также обновит столбец updated_at
, что позволяет знать, когда обновлять кэш
модели заметки Post
:
$comment = App\Comment::find(1);
$comment->text = 'Edit to this comment!';
$comment->save();