Skip to content

Eloquent: Отношения

1. Введение

Таблицы базы данных часто связаны друг с другом. Например, у записи в блоге может быть много комментариев, или заказ может быть связан с пользователем, который его оформил. Eloquent упрощает управление этими связями и работу с ними, поддерживая множество распространённых типов отношений:

2. Определение отношений

Отношения Eloquent определяются как методы в ваших Eloquent-моделях. Поскольку отношения также выполняют роль мощных конструкторов запросов, их определение в виде методов предоставляет широкие возможности для цепочек методов и наложения условий. Например, мы можем добавить дополнительные ограничения к запросу для этого отношения posts:

$user->posts()->where('active', 1)->get();

Но прежде чем углубляться в использование отношений, давайте изучим, как определить каждый тип отношений, поддерживаемых Eloquent.

2.1. Один к одному / Has One

Отношение "один к одному" является одним из самых простых типов отношений в базе данных. Например, модель User может быть связана с одной моделью Phone. Чтобы определить это отношение, мы создадим метод phone в модели User. Метод phone должен вызывать метод hasOne и возвращать его результат. Метод hasOne доступен в вашей модели через базовый класс модели Illuminate\Database\Eloquent\Model:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
 
class User extends Model
{
/**
* Получение телефона, связанного с пользователем.
*/
public function phone(): HasOne
{
return $this->hasOne(Phone::class);
}
}

Первым аргументом, передаваемым в метод hasOne, является имя связанной модели. После определения отношения мы можем получить связанную запись, используя динамические свойства Eloquent. Динамические свойства позволяют обращаться к методам отношений так, как будто они являются свойствами, определёнными в модели:

$phone = User::find(1)->phone;

Eloquent определяет внешний ключ отношения на основе имени родительской модели. В этом случае предполагается, что модель Phone автоматически содержит внешний ключ user_id. Если вы хотите изменить это соглашение, вы можете передать второй аргумент в метод hasOne:

return $this->hasOne(Phone::class, 'foreign_key');

Кроме того, Eloquent предполагает, что внешний ключ должен иметь значение, соответствующее столбцу первичного ключа родителя. Другими словами, Eloquent будет искать значение столбца id пользователя в столбце user_id записи Phone. Если вы хотите, чтобы отношение использовало значение первичного ключа, отличное от id или свойства $primaryKey вашей модели, вы можете передать третий аргумент в метод hasOne:

return $this->hasOne(Phone::class, 'foreign_key', 'local_key');

2.1.1. Определение обратного отношения

Итак, мы можем получить доступ к модели 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, указав пользовательский ключ родительской таблицы:

2.2. Один ко многим / Has Many

Отношение "один ко многим" используется для определения связей, где одна модель является родительской для одной или нескольких дочерних моделей. Например, у записи в блоге может быть бесконечное количество комментариев. Как и все остальные отношения в Eloquent, отношения "один ко многим" определяются через метод в вашей Eloquent-модели:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
 
class Post extends Model
{
/**
* Get the comments for the blog post.
*/
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}

Помните, что Eloquent автоматически определит правильный столбец внешнего ключа для модели Comment. По соглашению Eloquent возьмёт имя родительской модели в формате "snake case" и добавит к нему суффикс _id. Таким образом, в данном примере Eloquent будет считать, что столбец внешнего ключа в модели Comment называется post_id.

После того как метод отношения был определён, мы можем получить доступ к коллекции связанных комментариев через свойство comments. Помните, что поскольку Eloquent предоставляет "динамические свойства отношений", мы можем обращаться к методам отношений так, как будто они определены как свойства модели:

use App\Models\Post;
 
$comments = Post::find(1)->comments;
 
foreach ($comments as $comment) {
// ...
}

Поскольку все отношения также являются конструкторами запросов, вы можете добавлять дополнительные условия к запросу отношения, вызывая метод comments и продолжая цепочку условий для запроса:

$comment = Post::find(1)->comments()
->where('title', 'foo')
->first();

Как и в случае с методом hasOne, вы также можете переопределить внешние и локальные ключи, передав дополнительные аргументы в метод hasMany:

return $this->hasMany(Comment::class, 'foreign_key');
 
return $this->hasMany(Comment::class, 'foreign_key', 'local_key');

2.2.1. Автоматическое наполнение родительских моделей в дочерних

Даже при использовании жадной загрузки Eloquent, проблемы с запросами "N + 1" могут возникнуть, если вы пытаетесь получить доступ к родительской модели из дочерней модели, перебирая дочерние модели:

$posts = Post::with('comments')->get();
 
foreach ($posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->post->title;
}
}

В приведённом выше примере возникла проблема с запросами "N + 1", потому что, даже если комментарии были загружены жадно для каждой модели Post, Eloquent не заполняет автоматически родительскую модель Post для каждой дочерней модели Comment.

Если вы хотите, чтобы Eloquent автоматически заполнял родительские модели в их дочерние, вы можете вызвать метод chaperone при определении отношения hasMany:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
 
class Post extends Model
{
/**
* Получение комментариев для записи в блоге.
*/
public function comments(): HasMany
{
return $this->hasMany(Comment::class)->chaperone();
}
}

Или, если вы хотите включить автоматическое наполнение родительских моделей во время выполнения, вы можете вызвать модель chaperone при жадной загрузке отношения:

use App\Models\Post;
 
$posts = Post::with([
'comments' => fn ($comments) => $comments->chaperone(),
])->get();

2.3. Один ко многим (обратное) / Belongs To

Теперь, когда мы можем получить доступ ко всем комментариям записи, давайте определим отношение, позволяющее комментарию получить доступ к его родительской записи. Чтобы определить обратное отношение для hasMany, создайте метод отношения в дочерней модели, который вызывает метод belongsTo:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Comment extends Model
{
/**
* Получение записи, которой принадлежит комментарий.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}

После того как отношение было определено, мы можем получить родительскую запись комментария, обратившись к "динамическому свойству отношения" post:

use App\Models\Comment;
 
$comment = Comment::find(1);
 
return $comment->post->title;

В приведённом выше примере Eloquent попытается найти модель Post, у которой значение id совпадает со значением столбца post_id в модели Comment.

Eloquent определяет имя внешнего ключа по умолчанию, анализируя имя метода отношения и добавляя к нему суффикс _, за которым следует имя столбца первичного ключа родительской модели. Таким образом, в этом примере Eloquent будет считать, что внешний ключ модели Post в таблице comments называется post_id.

Однако, если внешний ключ для вашего отношения не соответствует этим соглашениям, вы можете передать собственное имя внешнего ключа в качестве второго аргумента методу belongsTo:

/**
* Получение записи, которой принадлежит комментарий.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key');
}

Если ваша родительская модель не использует id в качестве первичного ключа или вы хотите найти связанную модель, используя другой столбец, вы можете передать третий аргумент методу belongsTo, указав пользовательский ключ вашей родительской таблицы:

/**
* Получение записи, которой принадлежит комментарий.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');
}

2.3.1. Модели по умолчанию

Отношения belongsTo, hasOne, hasOneThrough и morphOne позволяют определить модель по умолчанию, которая будет возвращена, если указанное отношение имеет значение null. Этот подход часто называют шаблоном нулевого объекта и он помогает убрать условные проверки из вашего кода. В следующем примере отношение 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';
});
}

2.3.2. Запросы для отношений "Belongs To"

10. Обновление времени меток родителя

Когда модель определяет связь 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'];
 
/**
* Получeние поста, которому принадлежит комментарий.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}
exclamation

Метки времени родительской модели будут обновлены только в том случае, если дочерняя модель обновляется с использованием метода save Eloquent.