- Введение
- Определение связей
- Связи многие ко многим
- Полиморфные связи
- Динамические связи
- Запросы к связям
- Агрегация связанных моделей
- Жадная загрузка
- Вставка и обновление связанных моделей
- Обновление временных меток родительских моделей
#Введение
Таблицы базы данных часто связаны друг с другом. Например, у записи блога может быть много комментариев, или заказ может быть связан с пользователем, который его разместил. Eloquent упрощает управление и работу с такими связями и поддерживает различные распространённые типы связей:
#Определение связей
Связи Eloquent определяются как методы в классах ваших моделей Eloquent. Поскольку связи также выступают мощными query builders, определение связей как методов обеспечивает удобное цепочное вызовы и возможности построения запросов. Например, мы можем добавить дополнительные условия к запросу для связи posts:
$user->posts()->where('active', 1)->get();
Но прежде чем углубляться в использование связей, давайте изучим, как определить каждый тип связи, поддерживаемый Eloquent.
#Один к одному
Связь один к одному — это самый простой тип связи в базе данных. Например, модель 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 или свойства $primaryKey вашей модели, вы можете передать третий аргумент в метод 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');
}
#Один ко многим
Связь один ко многим используется для определения отношений, где одна модель является родительской для одной или нескольких дочерних моделей. Например, у записи блога может быть бесконечное количество комментариев. Как и все другие связи 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) {
// ...
}
Поскольку все связи также являются query builders, вы можете добавить дополнительные условия к запросу, вызвав метод 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');
#Один ко многим (обратная) / Принадлежит
Теперь, когда мы можем получить все комментарии записи, давайте определим связь, которая позволит комментарию получить родительскую запись. Чтобы определить обратную связь для 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 pattern и он помогает убрать условные проверки в коде. В следующем примере связь 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';
});
}
#Запросы к связям belongsTo
При запросе дочерних моделей для связи "принадлежит" вы можете вручную построить условие 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();
#Одно из многих
Иногда у модели может быть много связанных моделей, но нужно легко получить "последнюю" или "первую" связанную модель. Например, модель 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');
}
Поскольку PostgreSQL не поддерживает выполнение функции MAX для столбцов UUID, в настоящее время невозможно использовать связи one-of-many вместе со столбцами UUID в PostgreSQL.
#Преобразование связей "многие" в связи "один"
Часто при получении одной модели с помощью методов 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');
}
#Расширенные связи 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" определяет отношение один к одному с другой моделью. Однако эта связь указывает, что объявляющая модель может быть связана с одной моделью через третью модель.
Например, в приложении автосервиса каждая модель 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" предоставляет удобный способ доступа к отдалённым связям через промежуточную связь. Например, предположим, что мы создаём платформу развертывания, подобную Laravel Vapor. Модель Project может получить доступ ко многим моделям Deployment через промежуточную модель Environment. Используя этот пример, вы можете легко получить все развертывания для данного проекта. Рассмотрим таблицы, необходимые для определения этой связи:
projects
id - integer
name - string
environments
id - integer
project_id - integer
name - string
deployments
id - integer
environment_id - integer
commit_hash - string
Теперь, когда мы рассмотрели структуру таблиц для связи, определим связь в модели Project:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
class Project extends Model
{
/**
* Получить все развертывания для проекта.
*/
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(Deployment::class, Environment::class);
}
}
Первый аргумент, передаваемый в метод hasManyThrough, — это имя конечной модели, к которой мы хотим получить доступ, а второй аргумент — имя промежуточной модели.
Или, если соответствующие связи уже определены во всех моделях, участвующих в связи, вы можете определить "has-many-through" связь, вызвав метод through и передав имена этих связей. Например, если у модели Project есть связь environments, а у модели Environment — связь deployments, вы можете определить "has-many-through" связь между проектом и развертываниями так:
// Синтаксис с использованием строк...
return $this->through('environments')->has('deployments');
// Динамический синтаксис...
return $this->throughEnvironments()->hasDeployments();
Хотя в таблице модели Deployment нет столбца project_id, связь hasManyThrough предоставляет доступ к развертываниям проекта через $project->deployments. Для получения этих моделей Eloquent проверяет столбец project_id в таблице промежуточной модели Environment. После нахождения соответствующих ID окружений они используются для запроса таблицы модели Deployment.
#Основные соглашения
При выполнении запросов для отношений будут использоваться типичные соглашения Eloquent для внешних ключей. Если вы хотите настроить ключи отношения, вы можете передать их как третий и четвёртый аргументы в метод hasManyThrough. Третий аргумент — имя внешнего ключа на промежуточной модели. Четвёртый аргумент — имя внешнего ключа на конечной модели. Пятый аргумент — локальный ключ, а шестой аргумент — локальный ключ промежуточной модели:
class Project extends Model
{
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(
Deployment::class,
Environment::class,
'project_id', // Внешний ключ в таблице environments...
'environment_id', // Внешний ключ в таблице deployments...
'id', // Локальный ключ в таблице projects...
'id' // Локальный ключ в таблице environments...
);
}
}
Или, как обсуждалось ранее, если соответствующие отношения уже определены во всех моделях, участвующих в связи, вы можете удобно определить отношение "has-many-through", вызвав метод through и передав имена этих отношений. Этот подход позволяет повторно использовать соглашения о ключах, уже определённые в существующих отношениях:
// Синтаксис на основе строк...
return $this->through('environments')->has('deployments');
// Динамический синтаксис...
return $this->throughEnvironments()->hasDeployments();
#Отношения многие ко многим
Отношения многие ко многим немного сложнее, чем 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, вызовите метод withTimestamps при определении отношения:
return $this->belongsToMany(Role::class)->withTimestamps();
Промежуточные таблицы, использующие автоматически поддерживаемые временные метки 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');
#Сортировка запросов по столбцам промежуточной таблицы
Вы можете сортировать результаты запросов отношений belongsToMany с помощью метода orderByPivot. В следующем примере мы получим все последние значки пользователя:
return $this->belongsToMany(Badge::class)
->where('rank', 'gold')
->orderByPivot('created_at', 'desc');
#Определение пользовательских моделей промежуточной таблицы
Если вы хотите определить пользовательскую модель для представления промежуточной таблицы вашего отношения многие ко многим, вы можете вызвать метод using при определении отношения. Пользовательские модели 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
{
// ...
}
Модели pivot не могут использовать трейт SoftDeletes. Если вам нужно мягкое удаление записей pivot, рассмотрите возможность преобразования вашей модели pivot в полноценную модель Eloquent.
#Пользовательские модели pivot и автоинкрементные ID
Если вы определили отношение многие ко многим с пользовательской моделью pivot, и эта модель имеет автоинкрементный первичный ключ, убедитесь, что в классе вашей модели pivot свойство incrementing установлено в true.
/**
* Указывает, являются ли ID автоинкрементными.
*
* @var bool
*/
public $incrementing = true;
#Полиморфные отношения
Полиморфное отношение позволяет дочерней модели принадлежать более чем одному типу модели через одну связь. Например, представьте приложение, в котором пользователи могут делиться блог-постами и видео. В таком приложении модель Comment может принадлежать как модели Post, так и модели Video.
#Один к одному (полиморфное)
#Структура таблиц
Полиморфное отношение один к одному похоже на обычное отношение один к одному; однако дочерняя модель может принадлежать более чем одному типу модели через одну связь. Например, блог 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 содержит ID поста или пользователя, а imageable_type — имя класса родительской модели. Столбец imageable_type используется Eloquent для определения типа родительской модели при доступе к отношению imageable. В данном случае он будет содержать либо App\Models\Post, либо App\Models\User.
#Структура модели
Далее рассмотрим определения моделей, необходимые для построения этого отношения:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Image extends Model
{
/**
* Получить родительскую модель imageable (пользователь или пост).
*/
public function imageable(): MorphTo
{
return $this->morphTo();
}
}
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
class Post extends Model
{
/**
* Получить изображение поста.
*/
public function image(): MorphOne
{
return $this->morphOne(Image::class, 'imageable');
}
}
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
class User extends Model
{
/**
* Получить изображение пользователя.
*/
public function image(): MorphOne
{
return $this->morphOne(Image::class, 'imageable');
}
}
#Получение отношения
После определения таблиц и моделей вы можете получить доступ к отношениям через модели. Например, чтобы получить изображение поста, можно обратиться к динамическому свойству image:
use App\Models\Post;
$post = Post::find(1);
$image = $post->image;
Вы можете получить родительскую модель полиморфного отношения, обратившись к имени метода, который вызывает morphTo. В данном случае это метод imageable в модели Image. Поэтому мы обратимся к этому методу как к динамическому свойству:
use App\Models\Image;
$image = Image::find(1);
$imageable = $image->imageable;
Отношение imageable в модели Image вернёт либо экземпляр Post, либо User, в зависимости от того, какая модель является владельцем изображения.
#Соглашения по ключам
При необходимости вы можете указать имена столбцов "id" и "type", используемых вашей полиморфной дочерней моделью. При этом всегда передавайте имя отношения первым аргументом в метод morphTo. Обычно это значение совпадает с именем метода, поэтому можно использовать константу PHP __FUNCTION__:
/**
* Получить модель, к которой принадлежит изображение.
*/
public function imageable(): MorphTo
{
return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');
}
#Один ко многим (полиморфное)
#Структура таблиц
Полиморфное отношение один ко многим похоже на обычное отношение один ко многим; однако дочерняя модель может принадлежать более чем одному типу модели через одну связь. Например, представьте, что пользователи вашего приложения могут оставлять комментарии к постам и видео. Используя полиморфные отношения, вы можете использовать одну таблицу comments для хранения комментариев как к постам, так и к видео. Сначала рассмотрим структуру таблиц, необходимую для построения этого отношения:
posts
id - integer
title - string
body - text
videos
id - integer
title - string
url - string
comments
id - integer
body - text
commentable_id - integer
commentable_type - string
#Структура модели
Далее рассмотрим определения моделей, необходимые для построения этого отношения:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Comment extends Model
{
/**
* Получить родительскую модель commentable (пост или видео).
*/
public function commentable(): MorphTo
{
return $this->morphTo();
}
}
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Post extends Model
{
/**
* Получить все комментарии поста.
*/
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Video extends Model
{
/**
* Получить все комментарии видео.
*/
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
#Получение отношения
После определения таблиц и моделей вы можете получить доступ к отношениям через динамические свойства моделей. Например, чтобы получить все комментарии поста, можно использовать динамическое свойство comments:
use App\Models\Post;
$post = Post::find(1);
foreach ($post->comments as $comment) {
// ...
}
Вы также можете получить родительскую модель полиморфного дочернего объекта, обратившись к имени метода, который вызывает morphTo. В данном случае это метод commentable в модели Comment. Поэтому мы обратимся к этому методу как к динамическому свойству, чтобы получить родительскую модель комментария:
use App\Models\Comment;
$comment = Comment::find(1);
$commentable = $comment->commentable;
Отношение commentable в модели Comment вернёт либо экземпляр Post, либо Video, в зависимости от того, какая модель является родителем комментария.
#Один из многих (полиморфное)
Иногда у модели может быть много связанных моделей, но вы хотите легко получить "последнюю" или "первую" связанную модель. Например, модель User может быть связана со многими моделями Image, но вы хотите определить удобный способ взаимодействия с самым последним загруженным изображением пользователя. Это можно сделать с помощью отношения morphOne в сочетании с методами ofMany:
/**
* Получить самое последнее изображение пользователя.
*/
public function latestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->latestOfMany();
}
Аналогично, вы можете определить метод для получения "самого старого", или первого, связанного объекта отношения:
/**
* Получить самое старое изображение пользователя.
*/
public function oldestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->oldestOfMany();
}
По умолчанию методы latestOfMany и oldestOfMany получают последнюю или первую связанную модель на основе первичного ключа модели, который должен быть сортируемым. Однако иногда вы можете захотеть получить одну модель из большого отношения, используя другой критерий сортировки.
Например, с помощью метода ofMany вы можете получить самое "лайкаемое" изображение пользователя. Метод ofMany принимает сортируемый столбец в качестве первого аргумента и агрегатную функцию (min или max), которая применяется при запросе связанной модели:
/**
* Получить самое популярное изображение пользователя.
*/
public function bestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');
}
Можно создавать более сложные отношения "один из многих". Для дополнительной информации обратитесь к разделу has one of many documentation.
#Многие ко многим (полиморфное)
#Структура таблиц
Полиморфные отношения многие ко многим немного сложнее, чем отношения "morph one" и "morph many". Например, модели Post и Video могут иметь полиморфное отношение к модели Tag. Использование полиморфного отношения многие ко многим в этом случае позволит вашему приложению иметь одну таблицу уникальных тегов, которые могут быть связаны с постами или видео. Сначала рассмотрим структуру таблиц, необходимую для построения этого отношения:
posts
id - integer
name - string
videos
id - integer
name - string
tags
id - integer
name - string
taggables
tag_id - integer
taggable_id - integer
taggable_type - string
Перед тем как изучать полиморфные отношения многие ко многим, рекомендуется ознакомиться с документацией по обычным отношениям многие ко многим.
#Структура модели
Теперь мы готовы определить отношения в моделях. Модели Post и Video будут содержать метод tags, который вызывает метод morphToMany, предоставляемый базовым классом Eloquent.
Метод morphToMany принимает имя связанной модели и "имя отношения". Основываясь на имени промежуточной таблицы и ключах, мы будем называть это отношение "taggable":
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
class Post extends Model
{
/**
* Получить все теги для поста.
*/
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
}
#Определение обратного отношения
Далее в модели Tag следует определить метод для каждой из возможных родительских моделей. В этом примере мы определим методы posts и videos. Оба метода должны возвращать результат вызова morphedByMany.
Метод morphedByMany принимает имя связанной модели и "имя отношения". Основываясь на имени промежуточной таблицы и ключах, мы будем называть это отношение "taggable":
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
class Tag extends Model
{
/**
* Получить все посты, которым назначен этот тег.
*/
public function posts(): MorphToMany
{
return $this->morphedByMany(Post::class, 'taggable');
}
/**
* Получить все видео, которым назначен этот тег.
*/
public function videos(): MorphToMany
{
return $this->morphedByMany(Video::class, 'taggable');
}
}
#Получение отношения
После определения таблиц и моделей вы можете получить доступ к отношениям через модели. Например, чтобы получить все теги для поста, можно использовать динамическое свойство tags:
use App\Models\Post;
$post = Post::find(1);
foreach ($post->tags as $tag) {
// ...
}
Вы можете получить родителя полиморфного отношения из полиморфной дочерней модели, обратившись к имени метода, который выполняет вызов morphedByMany. В данном случае это методы posts или videos в модели Tag:
use App\Models\Tag;
$tag = Tag::find(1);
foreach ($tag->posts as $post) {
// ...
}
foreach ($tag->videos as $video) {
// ...
}
#Пользовательские полиморфные типы
По умолчанию Laravel использует полное имя класса для хранения "типа" связанной модели. Например, в приведённом выше примере отношения один-ко-многим, где модель Comment может принадлежать модели Post или Video, значение по умолчанию для commentable_type будет либо App\Models\Post, либо App\Models\Video. Однако вы можете захотеть отделить эти значения от внутренней структуры вашего приложения.
Например, вместо использования имён моделей в качестве "типа" можно использовать простые строки, такие как post и video. Таким образом, значения в столбце полиморфного "типа" в базе данных останутся валидными даже при переименовании моделей:
use Illuminate\Database\Eloquent\Relations\Relation;
Relation::enforceMorphMap([
'post' => 'App\Models\Post',
'video' => 'App\Models\Video',
]);
Вы можете вызвать метод enforceMorphMap в методе boot вашего класса App\Providers\AppServiceProvider или создать отдельный сервис-провайдер, если хотите.
Вы можете определить морф-алиас для данной модели во время выполнения, используя метод модели getMorphClass. Аналогично, вы можете определить полное имя класса, связанное с морф-алиасом, используя метод Relation::getMorphedModel:
use Illuminate\Database\Eloquent\Relations\Relation;
$alias = $post->getMorphClass();
$class = Relation::getMorphedModel($alias);
При добавлении "morph map" в существующее приложение все значения столбцов *_type в базе данных, которые содержат полные имена классов, должны быть преобразованы в соответствующие имена из карты.
#Динамические отношения
Вы можете использовать метод resolveRelationUsing для определения отношений между моделями Eloquent во время выполнения. Хотя это обычно не рекомендуется для обычной разработки приложений, иногда это может быть полезно при создании пакетов Laravel.
Метод resolveRelationUsing принимает имя желаемого отношения в качестве первого аргумента. Вторым аргументом должен быть замыкание, принимающее экземпляр модели и возвращающее корректное определение отношения Eloquent. Обычно динамические отношения настраиваются в методе boot сервис-провайдера:
use App\Models\Order;
use App\Models\Customer;
Order::resolveRelationUsing('customer', function (Order $orderModel) {
return $orderModel->belongsTo(Customer::class, 'customer_id');
});
При определении динамических отношений всегда указывайте явные имена ключей в методах отношений Eloquent.
#Запросы к отношениям
Поскольку все отношения Eloquent определяются через методы, вы можете вызвать эти методы, чтобы получить экземпляр отношения без фактического выполнения запроса для загрузки связанных моделей. Кроме того, все типы отношений Eloquent также выступают в роли query builder, позволяя добавлять дополнительные ограничения к запросу отношения перед его выполнением в базе данных.
Например, представьте блог, в котором модель User имеет множество связанных моделей Post:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Model
{
/**
* Получить все посты пользователя.
*/
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
}
Вы можете выполнить запрос к отношению posts и добавить дополнительные ограничения следующим образом:
use App\Models\User;
$user = User::find(1);
$user->posts()->where('active', 1)->get();
Вы можете использовать любые методы Laravel query builder на отношении, поэтому рекомендуем ознакомиться с документацией по query builder, чтобы узнать обо всех доступных методах.
#Цепочка orWhere после отношений
Как показано в примере выше, вы можете добавлять дополнительные ограничения к отношениям при их запросе. Однако будьте осторожны при добавлении orWhere к отношению, так как условия orWhere будут логически сгруппированы на том же уровне, что и ограничение отношения:
$user->posts()
->where('active', 1)
->orWhere('votes', '>=', 100)
->get();
Пример выше сгенерирует следующий SQL-запрос. Как видно, условие or заставляет запрос возвращать любой пост с количеством голосов больше 100. Запрос больше не ограничен конкретным пользователем:
select *
from posts
where user_id = ? and active = 1 or votes >= 100
В большинстве случаев следует использовать логические группы, чтобы сгруппировать условия в скобках:
use Illuminate\Database\Eloquent\Builder;
$user->posts()
->where(function (Builder $query) {
return $query->where('active', 1)
->orWhere('votes', '>=', 100);
})
->get();
Пример выше сгенерирует следующий SQL-запрос. Обратите внимание, что логическая группировка корректно объединила условия, и запрос остаётся ограниченным конкретным пользователем:
select *
from posts
where user_id = ? and (active = 1 or votes >= 100)
#Методы отношений и динамические свойства
Если вам не нужно добавлять дополнительные ограничения к запросу отношения Eloquent, вы можете обращаться к отношению как к свойству. Например, продолжая пример с моделями User и Post, мы можем получить все посты пользователя так:
use App\Models\User;
$user = User::find(1);
foreach ($user->posts as $post) {
// ...
}
Динамические свойства отношений выполняют "ленивую загрузку", то есть данные отношения загружаются только при первом обращении к свойству. Поэтому разработчики часто используют жадную загрузку, чтобы заранее загрузить отношения, которые будут использоваться после загрузки модели. Жадная загрузка значительно сокращает количество SQL-запросов, необходимых для загрузки отношений модели.
#Запросы по существованию отношений
При получении записей моделей вы можете ограничить результаты на основе существования отношения. Например, предположим, что вы хотите получить все посты блога, у которых есть хотя бы один комментарий. Для этого можно передать имя отношения в методы has и orHas:
use App\Models\Post;
// Получить все посты, у которых есть хотя бы один комментарий...
$posts = Post::has('comments')->get();
Вы также можете указать оператор и количество для более точной настройки запроса:
// Получить все посты, у которых три или более комментариев...
$posts = Post::has('comments', '>=', 3)->get();
Вложенные вызовы has можно строить с помощью "точечной" нотации. Например, можно получить все посты, у которых есть хотя бы один комментарий с хотя бы одним изображением:
// Получить посты, у которых есть хотя бы один комментарий с изображениями...
$posts = Post::has('comments.images')->get();
Если вам нужна ещё большая гибкость, вы можете использовать методы whereHas и orWhereHas для добавления дополнительных условий к запросам has, например, для проверки содержимого комментария:
use Illuminate\Database\Eloquent\Builder;
// Получить посты с хотя бы одним комментарием, содержащим слова, начинающиеся на code%...
$posts = Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
})->get();
// Получить посты с как минимум десятью комментариями, содержащими слова, начинающиеся на code%...
$posts = Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
}, '>=', 10)->get();
В настоящее время Eloquent не поддерживает запросы по существованию отношений между разными базами данных. Отношения должны находиться в одной базе данных.
#Встроенные запросы по существованию отношений
Если вы хотите выполнить запрос по существованию отношения с одним простым условием, может быть удобнее использовать методы whereRelation, orWhereRelation, whereMorphRelation и orWhereMorphRelation. Например, можно получить все посты с неподтверждёнными комментариями:
use App\Models\Post;
$posts = Post::whereRelation('comments', 'is_approved', false)->get();
Конечно, как и в методе where query builder, вы можете указать оператор:
$posts = Post::whereRelation(
'comments', 'created_at', '>=', now()->subHour()
)->get();
#Запросы по отсутствию отношений
При получении записей моделей вы можете ограничить результаты на основе отсутствия отношения. Например, предположим, что вы хотите получить все посты блога, у которых нет комментариев. Для этого можно передать имя отношения в методы doesntHave и orDoesntHave:
use App\Models\Post;
$posts = Post::doesntHave('comments')->get();
Если вам нужна ещё большая гибкость, вы можете использовать методы whereDoesntHave и orWhereDoesntHave для добавления дополнительных условий к запросам doesntHave, например, для проверки содержимого комментария:
use Illuminate\Database\Eloquent\Builder;
$posts = Post::whereDoesntHave('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
})->get();
Вы можете использовать "точечную" нотацию для выполнения запроса по вложенному отношению. Например, следующий запрос получит все посты без комментариев, но посты с комментариями от авторов, которые не заблокированы, будут включены в результаты:
use Illuminate\Database\Eloquent\Builder;
$posts = Post::whereDoesntHave('comments.author', function (Builder $query) {
$query->where('banned', 0);
})->get();
#Запросы к отношениям Morph To
Для запроса существования отношений "morph to" вы можете использовать методы whereHasMorph и whereDoesntHaveMorph. Эти методы принимают имя отношения в качестве первого аргумента. Далее указываются имена связанных моделей, которые вы хотите включить в запрос. Наконец, можно передать замыкание для настройки запроса отношения:
use App\Models\Comment;
use App\Models\Post;
use App\Models\Video;
use Illuminate\Database\Eloquent\Builder;
// Получить комментарии, связанные с постами или видео, у которых заголовок начинается на code%...
$comments = Comment::whereHasMorph(
'commentable',
[Post::class, Video::class],
function (Builder $query) {
$query->where('title', 'like', 'code%');
}
)->get();
// Получить комментарии, связанные с постами, у которых заголовок не начинается на code%...
$comments = Comment::whereDoesntHaveMorph(
'commentable',
Post::class,
function (Builder $query) {
$query->where('title', 'like', 'code%');
}
)->get();
Иногда может потребоваться добавить ограничения к запросу в зависимости от "типа" связанной полиморфной модели. Замыкание, передаваемое в метод whereHasMorph, может принимать второй аргумент $type. Этот аргумент позволяет проверить "тип" строящегося запроса:
use Illuminate\Database\Eloquent\Builder;
$comments = Comment::whereHasMorph(
'commentable',
[Post::class, Video::class],
function (Builder $query, string $type) {
$column = $type === Post::class ? 'content' : 'title';
$query->where($column, 'like', 'code%');
}
)->get();
#Запрос всех связанных моделей
Вместо передачи массива возможных полиморфных моделей вы можете указать * в качестве универсального значения. Это заставит Laravel получить все возможные полиморфные типы из базы данных. Для этого Laravel выполнит дополнительный запрос:
use Illuminate\Database\Eloquent\Builder;
$comments = Comment::whereHasMorph('commentable', '*', function (Builder $query) {
$query->where('title', 'like', 'foo%');
})->get();
#Агрегация связанных моделей
#Подсчёт связанных моделей
Иногда вы можете захотеть подсчитать количество связанных моделей для данного отношения, не загружая сами модели. Для этого вы можете использовать метод withCount. Метод withCount добавит атрибут {relation}_count к результирующим моделям:
use App\Models\Post;
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count;
}
Передавая массив в метод withCount, вы можете добавить подсчёты для нескольких отношений и добавить дополнительные ограничения к запросам:
use Illuminate\Database\Eloquent\Builder;
$posts = Post::withCount(['votes', 'comments' => function (Builder $query) {
$query->where('content', 'like', 'code%');
}])->get();
echo $posts[0]->votes_count;
echo $posts[0]->comments_count;
Вы также можете задать псевдоним для результата подсчёта, что позволяет выполнять несколько подсчётов по одному и тому же отношению:
use Illuminate\Database\Eloquent\Builder;
$posts = Post::withCount([
'comments',
'comments as pending_comments_count' => function (Builder $query) {
$query->where('approved', false);
},
])->get();
echo $posts[0]->comments_count;
echo $posts[0]->pending_comments_count;
#Отложенная загрузка подсчёта
С помощью метода loadCount вы можете загрузить подсчёт отношения после того, как родительская модель уже была получена:
$book = Book::first();
$book->loadCount('genres');
Если нужно задать дополнительные ограничения к запросу подсчёта, можно передать массив, ключами которого являются имена отношений, а значениями — замыкания с экземпляром query builder:
$book->loadCount(['reviews' => function (Builder $query) {
$query->where('rating', 5);
}])
#Подсчёт отношений и пользовательские select-запросы
Если вы комбинируете withCount с методом select, убедитесь, что вызываете withCount после select:
$posts = Post::select(['title', 'body'])
->withCount('comments')
->get();
#Другие агрегатные функции
Помимо метода withCount, Eloquent предоставляет методы withMin, withMax, withAvg, withSum и withExists. Они добавляют атрибуты вида {relation}_{function}_{column} к полученным моделям:
use App\Models\Post;
$posts = Post::withSum('comments', 'votes')->get();
foreach ($posts as $post) {
echo $post->comments_sum_votes;
}
Если вы хотите получить результат агрегатной функции под другим именем, можно указать собственный псевдоним:
$posts = Post::withSum('comments as total_comments', 'votes')->get();
foreach ($posts as $post) {
echo $post->total_comments;
}
Как и метод loadCount, доступны отложенные версии этих методов. Они позволяют выполнять агрегатные операции над моделями Eloquent, которые уже были загружены:
$post = Post::first();
$post->loadSum('comments', 'votes');
Если вы комбинируете эти агрегатные методы с select, убедитесь, что вызываете агрегатные методы после select:
$posts = Post::select(['title', 'body'])
->withExists('comments')
->get();
#Подсчёт связанных моделей в Morph To отношениях
Если вы хотите жадно загрузить отношение "morph to", а также подсчёты связанных моделей для различных сущностей, которые могут быть возвращены этим отношением, вы можете использовать метод with в сочетании с методом morphWithCount отношения morphTo.
В этом примере предположим, что модели Photo и Post могут создавать модели ActivityFeed. Предположим, что модель ActivityFeed определяет отношение "morph to" с именем parentable, которое позволяет получить родительскую модель Photo или Post для конкретного экземпляра ActivityFeed. Кроме того, предположим, что модели Photo имеют множество моделей Tag, а модели Post имеют множество моделей Comment.
Теперь представим, что мы хотим получить экземпляры ActivityFeed и выполнить жадную загрузку родительских моделей parentable для каждого экземпляра ActivityFeed. Кроме того, мы хотим получить количество тегов, связанных с каждой родительской фотографией, и количество комментариев, связанных с каждым родительским постом:
use Illuminate\Database\Eloquent\Relations\MorphTo;
$activities = ActivityFeed::with([
'parentable' => function (MorphTo $morphTo) {
$morphTo->morphWithCount([
Photo::class => ['tags'],
Post::class => ['comments'],
]);
}])->get();
#Отложенная загрузка подсчёта
Предположим, что мы уже получили набор моделей ActivityFeed и теперь хотим загрузить вложенные подсчёты отношений для различных моделей parentable, связанных с этими активностями. Для этого можно использовать метод loadMorphCount:
$activities = ActivityFeed::with('parentable')->get();
$activities->loadMorphCount('parentable', [
Photo::class => ['tags'],
Post::class => ['comments'],
]);
#Жадная загрузка
При обращении к отношениям Eloquent как к свойствам связанные модели загружаются "лениво". Это значит, что данные отношения не загружаются до первого обращения к свойству. Однако Eloquent может "жадно загружать" отношения в момент запроса родительской модели. Жадная загрузка решает проблему "N + 1" запросов. Чтобы проиллюстрировать проблему N + 1, рассмотрим модель Book, которая "принадлежит" модели Author:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Book extends Model
{
/**
* Получить автора книги.
*/
public function author(): BelongsTo
{
return $this->belongsTo(Author::class);
}
}
Теперь получим все книги и их авторов:
use App\Models\Book;
$books = Book::all();
foreach ($books as $book) {
echo $book->author->name;
}
Этот цикл выполнит один запрос для получения всех книг из таблицы, а затем отдельный запрос для каждого автора книги. Если книг 25, то будет выполнено 26 запросов: один для книг и 25 для авторов.
К счастью, мы можем использовать жадную загрузку, чтобы сократить количество запросов до двух. При построении запроса можно указать, какие отношения следует жадно загружать, используя метод with:
$books = Book::with('author')->get();
foreach ($books as $book) {
echo $book->author->name;
}
Для этой операции будет выполнено всего два запроса — один для получения всех книг и один для получения всех авторов всех книг:
select * from books
select * from authors where id in (1, 2, 3, 4, 5, ...)
#Жадная загрузка нескольких отношений
Иногда нужно жадно загрузить несколько разных отношений. Для этого передайте массив отношений в метод with:
$books = Book::with(['author', 'publisher'])->get();
#Вложенная жадная загрузка
Чтобы жадно загрузить отношения вложенных отношений, используйте "точечную" нотацию. Например, жадно загрузим всех авторов книг и все личные контакты авторов:
$books = Book::with('author.contacts')->get();
В качестве альтернативы можно указать вложенные отношения для жадной загрузки, передав вложенный массив в метод with, что удобно при жадной загрузке нескольких вложенных отношений:
$books = Book::with([
'author' => [
'contacts',
'publisher',
],
])->get();
#Вложенная жадная загрузка отношений morphTo
Если вы хотите жадно загрузить отношение morphTo, а также вложенные отношения для различных сущностей, которые могут быть возвращены этим отношением, используйте метод with вместе с методом morphWith отношения morphTo. Для примера рассмотрим следующую модель:
<?php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class ActivityFeed extends Model
{
/**
* Получить родителя записи ленты активности.
*/
public function parentable(): MorphTo
{
return $this->morphTo();
}
}
В этом примере предположим, что модели Event, Photo и Post могут создавать модели ActivityFeed. Кроме того, предположим, что модели Event принадлежат модели Calendar, модели Photo связаны с моделями Tag, а модели Post принадлежат модели Author.
Используя эти определения моделей и отношений, мы можем получить экземпляры ActivityFeed и жадно загрузить все модели parentable и их вложенные отношения:
use Illuminate\Database\Eloquent\Relations\MorphTo;
$activities = ActivityFeed::query()
->with(['parentable' => function (MorphTo $morphTo) {
$morphTo->morphWith([
Event::class => ['calendar'],
Photo::class => ['tags'],
Post::class => ['author'],
]);
}])->get();
#Жадная загрузка конкретных столбцов
Не всегда нужны все столбцы из связанных отношений. Поэтому Eloquent позволяет указать, какие именно столбцы отношения вы хотите получить:
$books = Book::with('author:id,name,book_id')->get();
При использовании этой возможности всегда включайте столбец id и все соответствующие внешние ключи в список загружаемых столбцов.
#Жадная загрузка по умолчанию
Иногда нужно всегда загружать некоторые отношения при получении модели. Для этого можно определить свойство $with в модели:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Book extends Model
{
/**
* Отношения, которые всегда должны загружаться.
*
* @var array
*/
protected $with = ['author'];
/**
* Получить автора книги.
*/
public function author(): BelongsTo
{
return $this->belongsTo(Author::class);
}
/**
* Получить жанр книги.
*/
public function genre(): BelongsTo
{
return $this->belongsTo(Genre::class);
}
}
Если вы хотите убрать элемент из свойства $with для одного запроса, используйте метод without:
$books = Book::without('author')->get();
Если вы хотите переопределить все элементы свойства $with для одного запроса, используйте метод withOnly:
$books = Book::withOnly('genre')->get();
#Ограничение жадной загрузки
Иногда нужно жадно загрузить отношение, но при этом задать дополнительные условия для запроса жадной загрузки. Это можно сделать, передав в метод with массив, где ключ — имя отношения, а значение — замыкание с дополнительными ограничениями:
use App\Models\User;
use Illuminate\Contracts\Database\Eloquent\Builder;
$users = User::with(['posts' => function (Builder $query) {
$query->where('title', 'like', '%code%');
}])->get();
В этом примере Eloquent жадно загрузит только те посты, у которых в столбце title содержится слово code. Вы можете использовать другие методы query builder для дальнейшей настройки операции жадной загрузки:
$users = User::with(['posts' => function (Builder $query) {
$query->orderBy('created_at', 'desc');
}])->get();
Методы конструктора запросов limit и take нельзя использовать при ограничении eager loading.
#Ограничение eager loading для отношений morphTo
Если вы выполняете eager loading для отношения morphTo, Eloquent выполнит несколько запросов для получения каждого типа связанной модели. Вы можете добавить дополнительные ограничения к каждому из этих запросов, используя метод constrain отношения MorphTo:
use Illuminate\Database\Eloquent\Relations\MorphTo;
$comments = Comment::with(['commentable' => function (MorphTo $morphTo) {
$morphTo->constrain([
Post::class => function ($query) {
$query->whereNull('hidden_at');
},
Video::class => function ($query) {
$query->where('type', 'educational');
},
]);
}])->get();
В этом примере Eloquent загрузит только посты, которые не были скрыты, и видео с полем type, равным "educational".
#Ограничение eager loading с проверкой существования отношений
Иногда необходимо проверить существование отношения и одновременно загрузить это отношение с теми же условиями. Например, вы можете захотеть получить только модели User, у которых есть дочерние модели Post, соответствующие заданному условию, и при этом eager load этих постов. Это можно сделать с помощью метода withWhereHas:
use App\Models\User;
$users = User::withWhereHas('posts', function ($query) {
$query->where('featured', true);
})->get();
#Ленивый eager loading
Иногда может потребоваться выполнить eager loading отношения после того, как родительская модель уже была получена. Это полезно, если нужно динамически решить, загружать ли связанные модели:
use App\Models\Book;
$books = Book::all();
if ($someCondition) {
$books->load('author', 'publisher');
}
Если нужно задать дополнительные ограничения для запроса eager loading, можно передать массив, где ключи — имена отношений, а значения — замыкания с экземпляром запроса:
$author->load(['books' => function (Builder $query) {
$query->orderBy('published_date', 'asc');
}]);
Чтобы загрузить отношение только если оно ещё не было загружено, используйте метод loadMissing:
$book->loadMissing('author');
#Вложенный ленивый eager loading и morphTo
Если вы хотите выполнить eager loading для отношения morphTo, а также для вложенных отношений у различных моделей, возвращаемых этим отношением, используйте метод loadMorph.
Этот метод принимает имя отношения morphTo в качестве первого аргумента и массив пар модель/отношение во втором. Для наглядности рассмотрим следующий пример модели:
<?php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class ActivityFeed extends Model
{
/**
* Получить родителя записи ленты активности.
*/
public function parentable(): MorphTo
{
return $this->morphTo();
}
}
В этом примере предположим, что модели Event, Photo и Post могут создавать модели ActivityFeed. Дополнительно предположим, что модели Event принадлежат модели Calendar, модели Photo связаны с моделями Tag, а модели Post принадлежат модели Author.
Используя эти определения моделей и отношений, мы можем получить экземпляры ActivityFeed и eager load всех моделей parentable и их вложенных отношений:
$activities = ActivityFeed::with('parentable')
->get()
->loadMorph('parentable', [
Event::class => ['calendar'],
Photo::class => ['tags'],
Post::class => ['author'],
]);
#Предотвращение ленивой загрузки
Как уже обсуждалось, eager loading отношений часто значительно улучшает производительность приложения. Поэтому вы можете запретить Laravel выполнять ленивую загрузку отношений. Для этого вызовите метод preventLazyLoading, предоставляемый базовым классом модели Eloquent. Обычно этот метод вызывают в методе boot класса AppServiceProvider вашего приложения.
Метод preventLazyLoading принимает необязательный булевый аргумент, указывающий, нужно ли запрещать ленивую загрузку. Например, вы можете отключить ленивую загрузку только в нерабочих средах, чтобы в продакшене приложение работало нормально, даже если случайно возникнет ленивое отношение:
use Illuminate\Database\Eloquent\Model;
/**
* Инициализация сервисов приложения.
*/
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}
После включения запрета ленивой загрузки Eloquent будет выбрасывать исключение Illuminate\Database\LazyLoadingViolationException, если приложение попытается выполнить ленивую загрузку любого отношения.
Вы можете настроить поведение при нарушениях ленивой загрузки с помощью метода handleLazyLoadingViolationsUsing. Например, с его помощью можно сделать так, чтобы нарушения ленивой загрузки только логировались, а не прерывали выполнение приложения исключениями:
Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {
$class = $model::class;
info("Attempted to lazy load [{$relation}] on model [{$class}].");
});
#Вставка и обновление связанных моделей
#Метод save
Eloquent предоставляет удобные методы для добавления новых моделей в отношения. Например, если нужно добавить новый комментарий к посту, вместо ручной установки атрибута post_id у модели Comment можно вставить комментарий через метод save отношения:
use App\Models\Comment;
use App\Models\Post;
$comment = new Comment(['message' => 'A new comment.']);
$post = Post::find(1);
$post->comments()->save($comment);
Обратите внимание, что мы не обращались к отношению comments как к динамическому свойству. Вместо этого вызвали метод comments, чтобы получить экземпляр отношения. Метод save автоматически добавит нужное значение post_id в новую модель Comment.
Если нужно сохранить несколько связанных моделей, используйте метод saveMany:
$post = Post::find(1);
$post->comments()->saveMany([
new Comment(['message' => 'A new comment.']),
new Comment(['message' => 'Another new comment.']),
]);
Методы save и saveMany сохраняют переданные экземпляры модели, но не добавляют вновь сохранённые модели в отношения, уже загруженные в память у родительской модели. Если вы планируете обращаться к этому отношению после использования методов save или saveMany, возможно, стоит вызвать метод refresh, чтобы перезагрузить модель и её отношения:
$post->comments()->save($comment);
$post->refresh();
// Все комментарии, включая только что сохранённый...
$post->comments;
#Рекурсивное сохранение моделей и отношений
Если вы хотите save свою модель и все её связанные отношения, можно использовать метод push. В этом примере модель Post будет сохранена вместе с комментариями и их авторами:
$post = Post::find(1);
$post->comments[0]->message = 'Message';
$post->comments[0]->author->name = 'Author Name';
$post->push();
Метод pushQuietly позволяет сохранить модель и связанные отношения без вызова событий:
$post->pushQuietly();
#Метод create
Помимо методов save и saveMany, вы также можете использовать метод create, который принимает массив атрибутов, создаёт модель и вставляет её в базу данных. Разница между save и create заключается в том, что save принимает полный экземпляр Eloquent-модели, тогда как create принимает простой PHP array. Только что созданная модель будет возвращена методом create:
use App\Models\Post;
$post = Post::find(1);
$comment = $post->comments()->create([
'message' => 'A new comment.',
]);
Для создания нескольких связанных моделей используйте метод createMany:
$post = Post::find(1);
$post->comments()->createMany([
['message' => 'A new comment.'],
['message' => 'Another new comment.'],
]);
Методы createQuietly и createManyQuietly создают модель(и) без генерации событий:
$user = User::find(1);
$user->posts()->createQuietly([
'title' => 'Post title.',
]);
$user->posts()->createManyQuietly([
['title' => 'First post.'],
['title' => 'Second post.'],
]);
Вы также можете использовать методы findOrNew, firstOrNew, firstOrCreate и updateOrCreate для создания и обновления моделей в отношениях.
Перед использованием метода create обязательно ознакомьтесь с документацией по массовому присвоению.
#Отношения Belongs To
Если нужно назначить дочернюю модель новому родителю, используйте метод associate. В этом примере модель User определяет отношение belongsTo к модели Account. Метод associate установит внешний ключ у дочерней модели:
use App\Models\Account;
$account = Account::find(10);
$user->account()->associate($account);
$user->save();
Чтобы удалить родителя у дочерней модели, используйте метод dissociate. Он установит внешний ключ отношения в null:
$user->account()->dissociate();
$user->save();
#Отношения Many to Many
#Присоединение / отсоединение
Eloquent также предоставляет методы для удобной работы с отношениями многие-ко-многим. Например, предположим, что у пользователя может быть много ролей, а у роли — много пользователей. Метод attach позволяет присоединить роль к пользователю, вставив запись в промежуточную таблицу:
use App\Models\User;
$user = 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 = User::find(1);
$user->roles()->detach([1, 2, 3]);
$user->roles()->attach([
1 => ['expires' => $expires],
2 => ['expires' => $expires],
]);
#Синхронизация связей
Вы также можете использовать метод sync для построения отношений многие ко многим. Метод sync принимает массив идентификаторов, которые будут помещены в сводную таблицу. Все идентификаторы, которых нет в указанном массиве, будут удалены из сводной таблицы. Таким образом, после завершения этой операции в сводной таблице останутся только идентификаторы из переданного массива:
$user->roles()->sync([1, 2, 3]);
Можно также передать дополнительные значения для промежуточной таблицы вместе с ID:
$user->roles()->sync([1 => ['expires' => true], 2, 3]);
Если нужно вставить одинаковые значения для всех синхронизируемых ID, используйте метод syncWithPivotValues:
$user->roles()->syncWithPivotValues([1, 2, 3], ['active' => true]);
Если не хотите отсоединять существующие ID, отсутствующие в массиве, используйте метод syncWithoutDetaching:
$user->roles()->syncWithoutDetaching([1, 2, 3]);
#Переключение связей
Отношение многие-ко-многим также предоставляет метод toggle, который переключает статус присоединения для указанных ID. Если ID присоединён, он будет отсоединён, и наоборот:
$user->roles()->toggle([1, 2, 3]);
Можно также передать дополнительные значения для промежуточной таблицы вместе с ID:
$user->roles()->toggle([
1 => ['expires' => true],
2 => ['expires' => true],
]);
#Обновление записи в промежуточной таблице
Если нужно обновить существующую запись в промежуточной таблице, используйте метод updateExistingPivot. Он принимает внешний ключ записи и массив атрибутов для обновления:
$user = User::find(1);
$user->roles()->updateExistingPivot($roleId, [
'active' => false,
]);
#Обновление временных меток родительской модели
Когда модель определяет отношение belongsTo или belongsToMany к другой модели, например, Comment, принадлежащий Post, иногда полезно обновлять временную метку родителя при обновлении дочерней модели.
Например, когда модель Comment обновляется, вы можете автоматически обновлять метку времени updated_at у владельческой модели Post, чтобы она устанавливалась на текущие дату и время. Для этого можно добавить в дочернюю модель свойство touches, содержащее имена отношений, у которых должны обновляться метки времени updated_at при обновлении дочерней модели:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Comment extends Model
{
/**
* Все отношения, у которых нужно обновлять временные метки.
*
* @var array
*/
protected $touches = ['post'];
/**
* Получить пост, к которому принадлежит комментарий.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}
Временные метки родительской модели обновляются только если дочерняя модель обновляется через метод save Eloquent.