Eloquent: Отношения
11.x
.
Почему это важно?
- Введение
- Определение отношений
- Один к одному
- Один ко многим
- Один ко многим (Обратное отношение)
- Многие ко многим
- Ко многим через
- Полиморфные отношения
- Полиморфные отношения многие ко многим
- Запросы к отношениям
- Методы отношений или динамические свойства
- Проверка существования связей при выборке
- Выборка по отсутствию отношения
- Подсчёт моделей в отношении
- Жадная загрузка
- Ограничение жадных загрузок
- Ленивая (отложенная) жадная загрузка
- Вставка и изменение связанных моделей
- Метод Save
- Метод Create
- Отношения "Принадлежит к"
- Отношения многие-ко-многим
- Привязка родительских меток времени
Введение
Ваши таблицы скорее всего как-то связаны с другими таблицами БД. Например, статья в блоге может иметь много комментариев, а заказ может быть связан с оставившим его пользователем. Eloquent упрощает работу и управление такими отношениями. Laravel поддерживает многие типы связей:
- Один к одному
- Один ко многим
- Многие ко многим
- Ко многим через
- Полиморфные отношения
- Полиморфные отношения многие ко многим
Определение отношений
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
{
/**
* Получить запись с номером телефона пользователя.
*/
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
{
/**
* Получить пользователя, владеющего данным телефоном.
*/
public function user()
{
return $this->belongsTo('App\User');
}
}
В примере выше Eloquent будет стараться искать связь между user_id
в модели Phone
и id
в модели User
. По умолчанию Eloquent определяет имя внешнего ключа по имени метода отношения, добавляя суффикс _id
. Однако, если имя внешнего ключа модели Phone
не user_id
, можно передать это имя вторым параметром в метод belongsTo
:
/**
* Получить пользователя, владеющего данным телефоном.
*/
public function user()
{
return $this->belongsTo('App\User', 'foreign_key');
}
Если ваша родительская модель не использует id
в качестве первичного ключа, или вам бы хотелось присоединить дочернюю модель к другому столбцу, вы можете передать третий параметр в метод belongsTo
, который определяет имя связанного столбца в родительской таблице:
/**
* Получить пользователя, владеющего данным телефоном.
*/
public function user()
{
return $this->belongsTo('App\User', 'foreign_key', 'other_key');
}
Модели по умолчанию
Отношение belongsTo
позволяет вам определить модель по умолчанию, которая будет возвращена в случае, если заданное отношение равно null
. Этот шаблон проектирования часто называют Null Object и он может помочь убрать проверку условий в вашем коде. В следующем примере отношение user
вернёт пустую модель App\User
, если к публикации не был присоединен ни один user
:
/**
* Получить автора публикации.
*/
public function user()
{
return $this->belongsTo('App\User')->withDefault();
}
Чтобы заполнить модель по умолчанию атрибутами, вы можете передать массив или функцию замыкания методу withDefault
:
/**
* Получить автора публикации.
*/
public function user()
{
return $this->belongsTo('App\User')->withDefault([
'name' => 'Guest Author',
]);
}
/**
* Получить автора публикации.
*/
public function user()
{
return $this->belongsTo('App\User')->withDefault(function ($user) {
$user->name = 'Guest Author';
});
}
Один ко многим
Отношение “один ко многим” используется для определения отношений, где одна модель владеет некоторым количеством других моделей. Примером отношения “один ко многим” является статья в блоге, которая имеет “много” комментариев. Как и другие отношения Eloquent вы можете смоделировать это отношение таким образом:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* Получить комментарии для публикации в блоге.
*/
public function comments()
{
return $this->hasMany('App\Comment');
}
}
Помните, что Eloquent автоматически определяет столбец внешнего ключа в модели Comment
. По соглашению, Eloquent возьмёт «snake case» названия владеющей модели и добавит к нему _id
. Таким образом, для данного примера, Eloquent предполагает, что внешним ключом для модели Comment
будет post_id
.
После определения отношения мы можем получить доступ к коллекции комментариев, обратившись к свойству comments
. Помните, что поскольку Eloquent поддерживает “динамические свойства”, мы можем обращаться к функциям отношений, как если бы они были определены свойством модели:
$comments = App\Post::find(1)->comments;
foreach ($comments as $comment) {
//
}
Конечно, так как отношения служат и в качестве конструкторов запросов, вы можете добавлять дополнительные условия к тем комментариям, которые получены вызовом метода comments
:
$comments = 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
{
/**
* Получить публикацию, которой принадлежит этот комментарий.
*/
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 определяет внешний ключ по имени метода отношения и _id
. Однако, если внешний ключ для модели Comment
не post_id
, вы можете передать своё имя вторым параметром в метод belongsTo
:
/**
* Получить публикацию, которой принадлежит этот комментарий.
*/
public function post()
{
return $this->belongsTo('App\Post', 'foreign_key');
}
Если ваша родительская модель не использует id
в качестве первичного ключа, или вам бы хотелось присоединить дочернюю модель к другому столбцу, вы можете передать третий параметр в метод belongsTo
, который определяет имя связанного столбца в родительской таблице:
/**
* Получить публикацию, которой принадлежит этот комментарий.
*/
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
.
Вы можете определить отношение «многие ко многим», написав метод, возвращающий результат метода belongsToMany
. К примеру, давайте определим метод roles
для нашей модели User
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Роли, принадлежащие пользователю.
*/
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
{
/**
* Пользователи, принадлежащие этой роли.
*/
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();
Фильтрация отношений через столбцы промежуточной таблицы
Вы также можете отфильтровать результаты, возвращённые методом 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
. Например, мы можем определить Role
, в которой используется пользовательская сводная модель UserRole
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
/**
* Пользователи, принадлежащие этой роли.
*/
public function users()
{
return $this->belongsToMany('App\User')->using('App\UserRole');
}
}
При определении модели UserRole
, мы будем наследовать класс Pivot
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Relations\Pivot;
class UserRole extends Pivot
{
//
}
Ко многим через
Связь «ко многим через» обеспечивает удобный короткий путь для доступа к удалённым отношениям через промежуточные. Например, модель 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
{
/**
* Получить все публикации для страны.
*/
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', 'user_id', 'id'
);
}
}
Полиморфные отношения
Структура таблицы
Полиморфные отношения позволяют модели быть связанной с более чем одной моделью. Например, предположим, пользователи вашего приложения могут комментировать и публикации и видео. Используя полиморфные отношения, вы можете использовать единственную таблицу 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
Главные поля, на которые нужно обратить внимание – commentable_id
и commentable_type
в таблице comments
. Столбец commentable_id
содержит ID статьи или видео, а commentable_type
— имя класса-модели владельца. Столбец commentable_type
позволяет ORM определить, какой класс модели должен быть возвращён при использовании отношения commentable
.
Структура модели
Теперь давайте рассмотрим, какие определения для модели нам нужны, чтобы построить её отношения:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
/**
* Получить все модели, владеющие commentable.
*/
public function commentable()
{
return $this->morphTo();
}
}
class Post extends Model
{
/**
* Получить все комментарии публикации.
*/
public function comments()
{
return $this->morphMany('App\Comment', 'commentable');
}
}
class Video extends Model
{
/**
* Получить все комментарии видео.
*/
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
, в зависимости от типа модели, которой принадлежит комментарий.
Пользовательские полиморфные типы
По умолчанию Laravel будет использовать полностью определённое имя класса для хранения типа связанной модели. Например, учитывая пример выше, где 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
или создать отдельный сервис-провайдер.
Полиморфные отношения многие ко многим
Структура таблиц
В дополнение к традиционным полиморфным связям вы можете также задать полиморфные связи многие ко многим. Например, модели блогов Post
и 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
будут иметь связь morphToMany
в базовом классе Eloquent через метод tags
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* Получить все теги публикации.
*/
public function tags()
{
return $this->morphToMany('App\Tag', 'taggable');
}
}
Определение обратного отношения
Теперь для модели Tag
вы должны определить метод для каждой из моделей отношения. Для нашего примера мы определим метод posts
и метод videos
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
/**
* Получить все публикации, связанные с тегом.
*/
public function posts()
{
return $this->morphedByMany('App\Post', 'taggable');
}
/**
* Получить все видео, которым присвоен этот тег.
*/
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) {
//
}
Запросы к отношениям
Поскольку все отношения определены функциями, мы можем вызывать эти функции, чтобы получить экземпляр отношения, фактически не выполняя запросы отношения. Кроме того, все типы Eloquent отношений также являются конструкторами запросов, позволяя вам сцеплять условия запроса отношения перед выполнением SQL-запроса к вашей базе данных.
Например, представьте систему блогов, в которой модель User
имеет множество связей с моделями Post
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Получить все публикации пользователя.
*/
public function posts()
{
return $this->hasMany('App\Post');
}
}
Вы можете запросить отношение posts
и добавить дополнительные условия к запросу отношения:
$user = App\User::find(1);
$user->posts()->where('active', 1)->get();
Вы можете использовать любой из методов конструктора запросов на отношении, поэтому не забудьте изучить документацию по конструктору запросов, где описаны все доступные вам методы.
Методы отношений или динамические свойства
Если вам не нужно добавлять дополнительные ограничения к Eloquent запросу отношения, вы можете просто получить доступ к отношению, как будто это свойство. Например, продолжая использовать наши модели User
и Post
в качестве примера, мы можем получить доступ ко всем сообщениям пользователя так:
$user = App\User::find(1);
foreach ($user->posts as $post) {
//
}
Динамические свойства поддерживают “отложенную (ленивую) загрузку”. Это означает, что они загрузят свои данные для отношения только в момент обращения к ним. Из-за этого разработчики часто используют жадную загрузку, чтобы предварительно загрузить отношения, для которых они знают, что доступ будет получен после загрузки модели. Жадная загрузка обеспечивает значительное сокращение SQL-запросов, которые должны быть выполнены, чтобы загрузить отношения модели.
Проверка существования связей при выборке
При чтении отношений модели вам может быть нужно ограничить результаты в зависимости от существования отношения. Например, вы хотите получить все публикации в блоге, имеющие хотя бы один комментарий. Для этого можно использовать метод has
:
// Получить все публикации в блоге, имеющие хотя бы один комментарий...
$posts = App\Post::has('comments')->get();
Вы также можете указать оператора и число, чтобы еще больше настроить запрос:
// Получить все публикации в блоге, имеющие три или более комментариев...
$posts = Post::has('comments', '>=', 3)->get();
Можно конструировать вложенные операторы has
с записи через точку. Например, вы можете получить все публикации, которые имеют хотя бы один комментарий и голос:
// Получить все публикации, которые имеют хотя бы один комментарий и голос...
$posts = Post::has('comments.votes')->get();
Если вам нужно ещё больше возможностей, вы можете использовать методы whereHas
и orWhereHas
, чтобы поместить условия “where” в ваши запросы has
. Эти методы позволяют вам добавить свои ограничения в отношения, такие как проверку содержимого комментария:
// Получить все публикации с хотя бы одним комментарием, имеющим слово foo%
$posts = Post::whereHas('comments', function ($query) {
$query->where('content', 'like', 'foo%');
})->get();
Выборка по отсутствию отношения
При получении записей модели бывает необходимо ограничить результаты выборки на основе отсутствия отношения. Например, если вы хотите получить все статьи, у которых нет комментариев. Для этого передайте имя отношения в метод doesntHave
:
$posts = App\Post::doesntHave('comments')->get();
Для ещё большего уточнения используйте метод whereDoesntHave
, чтобы добавить условия “where” в ваши запросы doesntHave
. Этот метод позволяет добавить дополнительные ограничения к отношению, например, проверку содержимого комментария:
$posts = Post::whereDoesntHave('comments', function ($query) {
$query->where('content', 'like', 'foo%');
})->get();
Подсчёт моделей в отношении
Если вы хотите посчитать число результатов отношения, не загружая их, используйте метод withCount
, который поместит столбец {relation}_count
в вашу результирующую модель. Например:
$posts = App\Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count;
}
Вы можете добавить “число” для нескольких отношений так же, как и добавить ограничения к запросам:
$posts = Post::withCount(['votes', 'comments' => function ($query) {
$query->where('content', 'like', 'foo%');
}])->get();
echo $posts[0]->votes_count;
echo $posts[0]->comments_count;
Вы также можете создавать псевдоним результата подсчета отношений, разрешая использовать несколько подсчетов на одном и том же отношении:
$posts = Post::withCount([
'comments',
'comments AS pending_comments' => function ($query) {
$query->where('approved', false);
}
])->get();
echo $posts[0]->comments_count;
echo $posts[0]->pending_comments_count;
Жадная загрузка
При доступе к Eloquent отношениям как к свойствам отношения “лениво загружаются”. Это означает, что данные отношения фактически не загружены, пока вы не обратитесь к свойству. Однако Eloquent может “жадно загружать” отношения в то время, когда вы запрашиваете родительскую модель. Жадная загрузка облегчает проблему N+1 запроса. Чтобы проиллюстрировать проблему N+1 запроса, рассмотрите модель Book
, которая связана с Author
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Book extends Model
{
/**
* Получить автора, который написал книгу.
*/
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;
}
Для данной операции будут выполнены только два запроса:
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();
Ограничение жадных загрузок
Иногда вам может понадобиться жадно загружать отношения. Но также иногда может понадобиться и определить дополнительные ограничения запроса для жадного запроса загрузки. Вот пример:
$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();
Ленивая (отложенная) жадная загрузка
Иногда вам, возможно, понадобится жадно загружать отношение после того, как родительская модель уже была получена. Например, это может быть полезно, если вы должны динамично решить, загружать ли связанные модели:
$books = App\Book::all();
if ($someCondition) {
$books->load('author', 'publisher');
}
Если вам нужно установить дополнительные ограничения запроса на жадно загружаемый запрос, вы можете передать массив, ключами которого будут отношения, которые необходимо загрузить. Значения массива должны быть экземплярами Closure
, которые получают экземпляр запроса:
$books->load(['author' => function ($query) {
$query->orderBy('published_date', 'asc');
}]);
Вставка и изменение связанных моделей
Метод 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.']),
]);
Метод Create
В дополнение к методам save
и saveMany
вы можете также использовать метод create
, который принимает массив атрибутов, создает модель и вставляет её в базу данных. Различие между save
и create
состоит в том, что save
принимает экземпляр Eloquent модели целиком, в то время как create
принимает простой PHP array
:
$post = App\Post::find(1);
$comment = $post->comments()->create([
'message' => 'A new comment.',
]);
Перед использованием метода
create
пересмотрите документацию по массовому назначению атрибутов.
Вы можете использовать метод createMany
для создания нескольких связанных моделей:
$post = App\Post::find(1);
$post->comments()->createMany([
[
'message' => 'A new comment.',
],
[
'message' => 'Another new comment.',
],
]);
Отношения "Принадлежит к"
При обновлении отношения belongsTo
вы можете использовать метод associate
. Этот метод установит внешний ключ на дочерней модели:
$account = App\Account::find(10);
$user->account()->associate($account);
$user->save();
При удалении отношения belongsTo
вы можете использовать метод dissociate
. Этот метод сбросит внешний ключ отношения в null
:
$user->account()->dissociate();
$user->save();
Отношения многие-ко-многим
Присоединение / Отсоединение
Также Eloquent предоставляет несколько дополнительных вспомогательных методов, чтобы сделать работу со связанными моделями более удобной. Например, давайте предположим, что у пользователя может быть много ролей, и у роли может быть много пользователей. Чтобы присоединить роль к пользователю вставкой записи в промежуточную таблицу, которая присоединяется к моделям, используйте метод attach
:
$user = App\User::find(1);
$user->roles()->attach($roleId);
При присоединении отношения к модели вы можете также передать массив дополнительных данных, которые будут вставлены в промежуточную таблицу:
$user->roles()->attach($roleId, ['expires' => $expires]);
Конечно, иногда может быть необходимо отсоединить роль от пользователя. Чтобы удалить запись отношения многие-ко-многим, используйте метод detach
. Метод detach
удалит соответствующую запись из промежуточной таблицы. Однако, обе модели останутся в базе данных:
// Отсоединить одну роль от пользователя...
$user->roles()->detach($roleId);
// Отсоединить все роли от пользователя...
$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, которые не находятся в данном массиве, будут удалены из промежуточной таблицы. После того как эта работа завершена, только ID из данного массива будут существовать в промежуточной таблице:
$user->roles()->sync([1, 2, 3]);
Также вы можете передать дополнительные значения промежуточной таблицы с массивом ID:
$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);
Привязка родительских меток времени
Когда модель имеет связь belongsTo
или belongsToMany
с другими моделями, например, Comment
, которая принадлежит Post
, иногда полезно обновить метку времени родителя, когда дочерняя модель обновлена. Например, когда модель Comment
обновлена, вы можете автоматически “привязать” метки времени updated_at
на владеющий ей Post
. Eloquent упрощает эту работу. Просто добавьте свойство touches, содержащее имена отношений к дочерней модели:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
/**
* Все отношения, которые предстоит привязать.
*
* @var array
*/
protected $touches = ['post'];
/**
* Получить публикацию, к которой привязан комментарий.
*/
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();