Eloquent: Отношения
- 1. Введение
- 2. Определение отношений
- Many to Many Relationships
- Polymorphic Relationships
- Dynamic Relationships
- Querying Relations
- Aggregating Related Models
- Eager Loading
- Inserting and Updating Related Models
- 10. Обновление времени меток родителя
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); }}
Метки времени родительской модели будут обновлены только в том случае, если дочерняя модель обновляется с использованием метода save
Eloquent.