コンテンツへスキップ

Eloquent: リレーションシップ

はじめに

データベーステーブルはしばしば互いに関連しています。例えば、ブログ記事には多くのコメントがあり、注文はそれを注文したユーザーに関連している可能性があります。Eloquentはこれらのリレーションシップの管理と操作を容易にし、さまざまな一般的なリレーションシップをサポートしています。

リレーションシップの定義

Eloquentリレーションシップは、Eloquentモデルクラスのメソッドとして定義されます。リレーションシップは強力なクエリビルダーとしても機能するため、メソッドとしてリレーションシップを定義することで、強力なメソッドチェーンとクエリ機能が提供されます。例えば、このpostsリレーションシップにさらにクエリ制約を連鎖させることができます。

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

しかし、リレーションシップの使用に深く入る前に、Eloquentがサポートする各タイプのリレーションシップを定義する方法を学びましょう。

1対1 / Has One

1対1リレーションシップは、非常に基本的なタイプのデータベースリレーションシップです。例えば、Userモデルは1つのPhoneモデルに関連付けられている場合があります。このリレーションシップを定義するには、Userモデルにphoneメソッドを配置します。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
{
/**
* Get the phone associated with the user.
*/
public function phone(): HasOne
{
return $this->hasOne(Phone::class);
}
}

hasOneメソッドに渡される最初の引数は、関連モデルクラスの名前です。リレーションシップが定義されると、Eloquentの動的プロパティを使用して関連レコードを取得できます。動的プロパティを使用すると、リレーションシップメソッドを、モデルに定義されているプロパティのようにアクセスできます。

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

Eloquentは、親モデル名に基づいてリレーションシップの外来キーを決定します。この場合、Phoneモデルにはuser_id外来キーがあると自動的に想定されます。この規則をオーバーライドする場合は、hasOneメソッドに2番目の引数を渡すことができます。

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

さらに、Eloquentは、外来キーの値が親の主キーカラムの値と一致する必要があると想定します。つまり、Eloquentは、Phoneレコードのuser_idカラムでユーザーのidカラムの値を検索します。id以外の主キー値を使用したい場合、またはモデルの$primaryKeyプロパティを使用したい場合は、hasOneメソッドに3番目の引数を渡すことができます。

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

リレーションシップの逆の定義

したがって、UserモデルからPhoneモデルにアクセスできます。次に、電話を所有するユーザーにアクセスできるように、Phoneモデルにリレーションシップを定義しましょう。hasOneリレーションシップの逆は、belongsToメソッドを使用して定義できます。

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Phone extends Model
{
/**
* Get the user that owns the phone.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

userメソッドを呼び出すと、EloquentはidPhoneモデルのuser_idカラムと一致するUserモデルを検索しようとします。

Eloquentは、リレーションシップメソッドの名前を調べ、メソッド名に_idを付加することで、外来キー名を決定します。したがって、この場合、EloquentはPhoneモデルにuser_idカラムがあると想定します。ただし、Phoneモデルの外来キーがuser_idではない場合は、belongsToメソッドに2番目の引数としてカスタムキー名を渡すことができます。

/**
* Get the user that owns the phone.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key');
}

親モデルがidを主キーとして使用していない場合、または別のカラムを使用して関連モデルを検索したい場合は、親テーブルのカスタムキーを指定する3番目の引数をbelongsToメソッドに渡すことができます。

/**
* Get the user that owns the phone.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
}

1対多 / Has Many

1対多リレーションシップは、単一のモデルが1つ以上の子モデルの親であるリレーションシップを定義するために使用されます。例えば、ブログ記事には無数のコメントがある可能性があります。他のすべてのEloquentリレーションシップと同様に、1対多リレーションシップは、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は親モデルの「スネークケース」名を取り、_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');

子モデルでの親モデルの自動ハイドレーション

Eloquent の eager loading を使用していても、子モデルをループ処理中に親モデルにアクセスしようとすると、「N + 1」クエリの問題が発生する可能性があります。

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

上記の例では、「N + 1」クエリの問題が発生しています。これは、すべての `Post` モデルに対してコメントが eager loading されていたにもかかわらず、Eloquent が各子 `Comment` モデルに親 `Post` を自動的に hydration しないためです。

`hasMany` リレーションシップを定義する際に `chaperone` メソッドを呼び出すと、Eloquent が子モデルに親モデルを自動的に hydration するようになります。

<?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)->chaperone();
}
}

あるいは、実行時に自動的な親モデルの hydration を有効にするには、リレーションシップを eager loading する際に `chaperone` メソッドを呼び出すことができます。

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

1対多(逆)/ Belongs To

投稿のすべてのコメントにアクセスできるようになったので、コメントが親の投稿にアクセスできるようにリレーションシップを定義しましょう。 `hasMany` リレーションシップの逆を定義するには、子モデルに `belongsTo` メソッドを呼び出すリレーションシップメソッドを定義します。

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Comment extends Model
{
/**
* Get the post that owns the comment.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}

リレーションシップを定義したら、`post` という「動的なリレーションシッププロパティ」にアクセスすることで、コメントの親投稿を取得できます。

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

上記の例では、Eloquent は `Comment` モデルの `post_id` カラムと一致する `id` を持つ `Post` モデルを検索しようとします。

Eloquent は、リレーションシップメソッドの名前を調べ、メソッド名にアンダースコア `_` と親モデルの主キーカラム名を付加することで、デフォルトの外部キー名を決定します。そのため、この例では、Eloquent は `comments` テーブル上の `Post` モデルの外部キーが `post_id` であると想定します。

ただし、リレーションシップの外部キーがこれらの規則に従っていない場合は、 `belongsTo` メソッドの第2引数としてカスタムの外部キー名を渡すことができます。

/**
* Get the post that owns the comment.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key');
}

親モデルが主キーとして `id` を使用していない場合、または別のカラムを使用して関連モデルを検索する場合は、 `belongsTo` メソッドに第3引数を渡して、親テーブルのカスタムキーを指定できます。

/**
* Get the post that owns the comment.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');
}

デフォルトモデル

`belongsTo` 、 `hasOne` 、 `hasOneThrough` 、および `morphOne` リレーションシップでは、指定されたリレーションシップが `null` の場合に返されるデフォルトモデルを定義できます。このパターンは、多くの場合、 Null Object パターン と呼ばれ、コード内の条件チェックを減らすのに役立ちます。次の例では、ユーザーが `Post` モデルにアタッチされていない場合、 `user` リレーションは空の `App\Models\User` モデルを返します。

/**
* Get the author of the post.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault();
}

デフォルトモデルに属性を設定するには、配列またはクロージャを `withDefault` メソッドに渡します。

/**
* Get the author of the post.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault([
'name' => 'Guest Author',
]);
}
 
/**
* Get the author of the post.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault(function (User $user, Post $post) {
$user->name = 'Guest Author';
});
}

Belongs To リレーションシップのクエリ

「belongs to」リレーションシップの子をクエリする際には、対応する Eloquent モデルを取得するために `where` 句を手動で構築できます。

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` メソッドの第2引数としてリレーションシップ名を手動で指定することもできます。

$posts = Post::whereBelongsTo($user, 'author')->get();

Has One of Many

モデルには多くの関連モデルがある場合がありますが、「最新」または「最古」の関連モデルを簡単に取得したい場合があります。たとえば、 `User` モデルは多くの `Order` モデルに関連付けられている可能性がありますが、ユーザーが最後に配置した最新の注文とやり取りする便利な方法を定義したい場合があります。これは、 `ofMany` メソッドと組み合わせて `hasOne` リレーションシップタイプを使用することで実現できます。

/**
* Get the user's most recent order.
*/
public function latestOrder(): HasOne
{
return $this->hasOne(Order::class)->latestOfMany();
}

同様に、「最古」または最初の関連モデルを取得するメソッドを定義することもできます。

/**
* Get the user's oldest order.
*/
public function oldestOrder(): HasOne
{
return $this->hasOne(Order::class)->oldestOfMany();
}

デフォルトでは、 `latestOfMany` メソッドと `oldestOfMany` メソッドは、ソート可能なモデルの主キーに基づいて最新または最古の関連モデルを取得します。ただし、場合によっては、異なるソート基準を使用して、より大きなリレーションシップから単一のモデルを取得したい場合があります。

たとえば、 `ofMany` メソッドを使用して、ユーザーの最も高価な注文を取得できます。 `ofMany` メソッドは、ソート可能なカラムを最初の引数として、関連モデルのクエリに適用する集計関数( `min` または `max` )を第2引数として受け取ります。

/**
* Get the user's largest order.
*/
public function largestOrder(): HasOne
{
return $this->hasOne(Order::class)->ofMany('price', 'max');
}
exclamation

PostgreSQL は UUID カラムに対して `MAX` 関数を実行できないため、現在、PostgreSQL の UUID カラムと one-of-many リレーションシップを組み合わせることはできません。

「Many」リレーションシップを Has One リレーションシップに変換する

`latestOfMany` 、 `oldestOfMany` 、または `ofMany` メソッドを使用して単一のモデルを取得する場合、多くの場合、同じモデルに対して「has many」リレーションシップが既に定義されています。便宜上、Laravel では、リレーションシップに対して `one` メソッドを呼び出すことで、このリレーションシップを「has one」リレーションシップに簡単に変換できます。

/**
* Get the user's orders.
*/
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
 
/**
* Get the user's largest order.
*/
public function largestOrder(): HasOne
{
return $this->orders()->one()->ofMany('price', 'max');
}

高度な Has One of Many リレーションシップ

より高度な「has one of many」リレーションシップを構築できます。たとえば、 `Product` モデルには、新しい価格が公開された後もシステムに保持される多くの関連 `Price` モデルがある可能性があります。さらに、製品の新しい価格データは、 `published_at` カラムを介して将来の有効日になるように事前に公開できます。

つまり、公開日が将来ではない最新の公開価格を取得する必要があります。さらに、2つの価格が同じ公開日を持つ場合、IDが大きい価格を優先します。これを実現するには、最新の価格を決定するソート可能なカラムを含む配列を `ofMany` メソッドに渡す必要があります。さらに、クロージャが `ofMany` メソッドの第2引数として提供されます。このクロージャは、リレーションシップクエリに追加の公開日制約を追加する役割を果たします。

/**
* Get the current pricing for the product.
*/
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

「has-one-through」リレーションシップは、別のモデルとの一対一の関連を定義します。ただし、このリレーションシップは、宣言するモデルを、第3モデルを介して別のモデルの1つのインスタンスと一致させることができることを示しています。

たとえば、車両修理ショップアプリケーションでは、各 `Mechanic` モデルは1つの `Car` モデルに関連付けられ、各 `Car` モデルは1つの `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
{
/**
* Get the car's owner.
*/
public function carOwner(): HasOneThrough
{
return $this->hasOneThrough(Owner::class, Car::class);
}
}

`hasOneThrough` メソッドに渡される最初の引数は、アクセスする最終モデルの名前であり、2番目の引数は中間モデルの名前です。

または、関連するリレーションシップが関係するすべてのモデルに既に定義されている場合は、 `through` メソッドを呼び出してそれらのリレーションシップの名前を指定することで、「has-one-through」リレーションシップを流暢に定義できます。たとえば、 `Mechanic` モデルに `cars` リレーションシップがあり、 `Car` モデルに `owner` リレーションシップがある場合、「has-one-through」リレーションシップを次のように定義してメカニックとオーナーを接続できます。

// String based syntax...
return $this->through('cars')->has('owner');
 
// Dynamic syntax...
return $this->throughCars()->hasOwner();

キーの規則

リレーションシップのクエリを実行する際には、一般的な Eloquent 外部キーの規則が使用されます。リレーションシップのキーをカスタマイズする場合は、 `hasOneThrough` メソッドの第3引数と第4引数として渡すことができます。第3引数は中間モデルの外部キーの名前です。第4引数は最終モデルの外部キーの名前です。第5引数はローカルキー、第6引数は中間モデルのローカルキーです。

class Mechanic extends Model
{
/**
* Get the car's owner.
*/
public function carOwner(): HasOneThrough
{
return $this->hasOneThrough(
Owner::class,
Car::class,
'mechanic_id', // Foreign key on the cars table...
'car_id', // Foreign key on the owners table...
'id', // Local key on the mechanics table...
'id' // Local key on the cars table...
);
}
}

または、前述のように、関連するリレーションシップが関係するすべてのモデルに既に定義されている場合は、 `through` メソッドを呼び出してそれらのリレーションシップの名前を指定することで、「has-one-through」リレーションシップを流暢に定義できます。このアプローチは、既存のリレーションシップに既に定義されているキーの規則を再利用するという利点があります。

// String based syntax...
return $this->through('cars')->has('owner');
 
// Dynamic syntax...
return $this->throughCars()->hasOwner();

Has Many Through

「has-many-through」リレーションシップは、中間リレーションを介して遠隔関係にアクセスする便利な方法を提供します。たとえば、 Laravel Vapor のようなデプロイメントプラットフォームを構築しているとしましょう。 `Project` モデルは、中間 `Environment` モデルを介して多くの `Deployment` モデルにアクセスできます。この例を使用すると、特定のプロジェクトのすべてのデプロイメントを簡単に収集できます。このリレーションシップを定義するために必要なテーブルを見てみましょう。

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
{
/**
* Get all of the deployments for the project.
*/
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(Deployment::class, Environment::class);
}
}

`hasManyThrough` メソッドに渡される最初の引数は、アクセスする最終モデルの名前であり、2番目の引数は中間モデルの名前です。

または、関連するリレーションシップが関係するすべてのモデルに既に定義されている場合は、 `through` メソッドを呼び出してそれらのリレーションシップの名前を指定することで、「has-many-through」リレーションシップを流暢に定義できます。たとえば、 `Project` モデルに `environments` リレーションシップがあり、 `Environment` モデルに `deployments` リレーションシップがある場合、「has-many-through」リレーションシップを次のように定義してプロジェクトとデプロイメントを接続できます。

// String based syntax...
return $this->through('environments')->has('deployments');
 
// Dynamic syntax...
return $this->throughEnvironments()->hasDeployments();

`Deployment` モデルのテーブルには `project_id` カラムが含まれていませんが、 `hasManyThrough` リレーションシップは `$project->deployments` を介してプロジェクトのデプロイメントへのアクセスを提供します。これらのモデルを取得するために、Eloquent は中間 `Environment` モデルのテーブルの `project_id` カラムを調べます。関連する環境IDが見つかった後、それらを使用して `Deployment` モデルのテーブルをクエリします。

キーの規則

リレーションシップのクエリを実行する際には、一般的な Eloquent 外部キーの規則が使用されます。リレーションシップのキーをカスタマイズする場合は、 `hasManyThrough` メソッドの第3引数と第4引数として渡すことができます。第3引数は中間モデルの外部キーの名前です。第4引数は最終モデルの外部キーの名前です。第5引数はローカルキー、第6引数は中間モデルのローカルキーです。

class Project extends Model
{
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(
Deployment::class,
Environment::class,
'project_id', // Foreign key on the environments table...
'environment_id', // Foreign key on the deployments table...
'id', // Local key on the projects table...
'id' // Local key on the environments table...
);
}
}

または、前述のように、関連するリレーションシップが関係するすべてのモデルに既に定義されている場合は、 `through` メソッドを呼び出してそれらのリレーションシップの名前を指定することで、「has-many-through」リレーションシップを流暢に定義できます。このアプローチは、既存のリレーションシップに既に定義されているキーの規則を再利用するという利点があります。

// String based syntax...
return $this->through('environments')->has('deployments');
 
// Dynamic syntax...
return $this->throughEnvironments()->hasDeployments();

多対多リレーションシップ

多対多のリレーションシップは、 `hasOne` と `hasMany` リレーションシップよりも少し複雑です。多対多のリレーションシップの例としては、多くの役割を持つユーザーと、その役割がアプリケーション内の他のユーザーと共有されるユーザーがあります。たとえば、ユーザーには「作成者」と「編集者」の役割が割り当てられる可能性がありますが、これらの役割は他のユーザーにも割り当てられる可能性があります。したがって、ユーザーには多くの役割があり、役割には多くのユーザーがいます。

テーブル構造

このリレーションシップを定義するには、 `users` 、 `roles` 、および `role_user` の3つのデータベーステーブルが必要です。 `role_user` テーブルは、関連モデル名のアルファベット順から派生し、 `user_id` と `role_id` カラムが含まれています。このテーブルは、ユーザーと役割をリンクする中間テーブルとして使用されます。

役割は多くのユーザーに属しているので、 `roles` テーブルに `user_id` カラムを配置することはできません。これは、役割が単一のユーザーのみに属することを意味します。役割が複数のユーザーに割り当てられるようにするには、 `role_user` テーブルが必要です。リレーションシップのテーブル構造は次のようになります。

users
id - integer
name - string
 
roles
id - integer
name - string
 
role_user
user_id - integer
role_id - integer

モデル構造

多対多のリレーションシップは、belongsToManyメソッドの結果を返すメソッドを記述することで定義されます。belongsToManyメソッドは、アプリケーションのすべてのEloquentモデルで使用されるIlluminate\Database\Eloquent\Modelベースクラスによって提供されます。例として、Userモデルにrolesメソッドを定義してみましょう。このメソッドに渡される最初の引数は、関連モデルクラスの名前です。

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
class User extends Model
{
/**
* The roles that belong to the user.
*/
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は2つの関連モデル名をアルファベット順に結合します。ただし、この規則をオーバーライドすることもできます。belongsToManyメソッドに2番目の引数を渡すことで、これを行うことができます。

return $this->belongsToMany(Role::class, 'role_user');

中間テーブルの名前をカスタマイズすることに加えて、belongsToManyメソッドに追加の引数を渡すことで、テーブル上のキーの列名もカスタマイズできます。3番目の引数は、リレーションシップを定義しているモデルの外部キー名であり、4番目の引数は、結合しているモデルの外部キー名です。

return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');

リレーションシップの逆の定義

多対多のリレーションシップの「逆」を定義するには、belongsToManyメソッドの結果も返す関連モデルにメソッドを定義する必要があります。ユーザー/ロールの例を完成させるために、Roleモデルにusersメソッドを定義しましょう。

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
class Role extends Model
{
/**
* The users that belong to the role.
*/
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
}

ご覧のとおり、App\Models\Userモデルを参照する点を除いて、リレーションシップは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');

Eloquentによって自動的に管理されるcreated_atupdated_atのタイムスタンプを中間テーブルに含めたい場合は、リレーションシップを定義する際にwithTimestampsメソッドを呼び出します。

return $this->belongsToMany(Role::class)->withTimestamps();
exclamation

Eloquentによって自動的に管理されるタイムスタンプを使用する中間テーブルには、created_atupdated_atのタイムスタンプ列の両方が必要です。

pivot属性名のカスタマイズ

前述のように、中間テーブルの属性は、pivot属性を介してモデル上でアクセスできます。ただし、アプリケーション内での目的をより適切に反映するように、この属性の名前をカスタマイズできます。

たとえば、アプリケーションにポッドキャストを購読できるユーザーが含まれている場合、ユーザーとポッドキャスト間に多対多のリレーションシップが存在する可能性があります。この場合、中間テーブル属性の名前をpivotではなくsubscriptionに変更することをお勧めします。これは、リレーションシップを定義する際に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リレーションシップクエリによって返される結果を、リレーションシップを定義する際にwherePivotwherePivotInwherePivotNotInwherePivotBetweenwherePivotNotBetweenwherePivotNull、および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メソッドを呼び出すことができます。カスタムピボットモデルを使用すると、メソッドやキャストなど、ピボットモデルに追加の動作を定義できます。

カスタムの多対多ピボットモデルはIlluminate\Database\Eloquent\Relations\Pivotクラスを拡張する必要がありますが、カスタムのポリモーフィック多対多ピボットモデルはIlluminate\Database\Eloquent\Relations\MorphPivotクラスを拡張する必要があります。例として、カスタムRoleUserピボットモデルを使用するRoleモデルを定義できます。

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
class Role extends Model
{
/**
* The users that belong to the role.
*/
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
{
// ...
}
exclamation

ピボットモデルはSoftDeletesトレイトを使用できません。ピボットレコードをソフト削除する必要がある場合は、ピボットモデルを実際のEloquentモデルに変換することを検討してください。

カスタムピボットモデルと自動増分ID

カスタムピボットモデルを使用し、そのピボットモデルが自動増分主キーを持つ多対多のリレーションシップを定義した場合は、カスタムピボットモデルクラスがtrueに設定されたincrementingプロパティを定義していることを確認する必要があります。

/**
* Indicates if the IDs are auto-incrementing.
*
* @var bool
*/
public $incrementing = true;

ポリモーフィックリレーションシップ

ポリモーフィックリレーションシップでは、子モデルは単一の関連付けを使用して複数のタイプのモデルに属することができます。たとえば、ユーザーがブログ記事とビデオを共有できるアプリケーションを構築しているとします。このようなアプリケーションでは、CommentモデルはPostモデルとVideoモデルの両方に属する可能性があります。

一対一(ポリモーフィック)

テーブル構造

一対一のポリモーフィックリレーションシップは、典型的な一対一のリレーションシップに似ていますが、子モデルは単一の関連付けを使用して複数のタイプのモデルに属することができます。たとえば、ブログのPostUserは、Imageモデルとのポリモーフィックリレーションシップを共有できます。一対一のポリモーフィックリレーションシップを使用すると、投稿やユーザーに関連付けることができる一意の画像の単一のテーブルを持つことができます。まず、テーブル構造を見てみましょう。

posts
id - integer
name - string
 
users
id - integer
name - string
 
images
id - integer
url - string
imageable_id - integer
imageable_type - string

imagesテーブルのimageable_id列とimageable_type列に注目してください。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
{
/**
* Get the parent imageable model (user or post).
*/
public function imageable(): MorphTo
{
return $this->morphTo();
}
}
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
 
class Post extends Model
{
/**
* Get the post's image.
*/
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
{
/**
* Get the user's image.
*/
public function image(): MorphOne
{
return $this->morphOne(Image::class, 'imageable');
}
}

リレーションシップの取得

データベーステーブルとモデルが定義されたら、モデルを介してリレーションシップにアクセスできます。たとえば、投稿の画像を取得するには、image動的リレーションシッププロパティにアクセスできます。

use App\Models\Post;
 
$post = Post::find(1);
 
$image = $post->image;

morphToを呼び出すメソッドの名前をアクセスすることで、ポリモーフィックモデルの親を取得できます。この場合は、Imageモデルのimageableメソッドです。そのため、動的リレーションシッププロパティとしてそのメソッドにアクセスします。

use App\Models\Image;
 
$image = Image::find(1);
 
$imageable = $image->imageable;

Imageモデルのimageableリレーションは、どのタイプのモデルが画像を所有しているかに応じて、PostインスタンスまたはUserインスタンスを返します。

キーの規則

必要に応じて、ポリモーフィック子モデルで使用される「id」列と「type」列の名前を指定できます。その場合は、常にリレーションシップの名前をmorphToメソッドの最初の引数として渡してください。通常、この値はメソッド名と一致する必要があるため、PHPの__FUNCTION__定数を使用できます。

/**
* Get the model that the image belongs to.
*/
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
{
/**
* Get the parent commentable model (post or video).
*/
public function commentable(): MorphTo
{
return $this->morphTo();
}
}
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
 
class Post extends Model
{
/**
* Get all of the post's comments.
*/
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
{
/**
* Get all of the video's comments.
*/
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を呼び出すメソッドの名前をアクセスすることで、ポリモーフィック子モデルの親を取得することもできます。この場合は、Commentモデルのcommentableメソッドです。そのため、コメントの親モデルにアクセスするために、動的リレーションシッププロパティとしてそのメソッドにアクセスします。

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

Commentモデルのcommentableリレーションは、どのタイプのモデルがコメントの親であるかに応じて、PostインスタンスまたはVideoインスタンスを返します。

子モデルでの親モデルの自動ハイドレーション

Eloquent の eager loading を使用していても、子モデルをループ処理中に親モデルにアクセスしようとすると、「N + 1」クエリの問題が発生する可能性があります。

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

上記の例では、「N + 1」クエリの問題が発生しています。これは、すべての `Post` モデルに対してコメントが eager loading されていたにもかかわらず、Eloquent が各子 `Comment` モデルに親 `Post` を自動的に hydration しないためです。

Eloquentに親モデルを子モデルに自動的にハイドレートさせたい場合は、morphManyリレーションシップを定義する際にchaperoneメソッドを呼び出すことができます。

class Post extends Model
{
/**
* Get all of the post's comments.
*/
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable')->chaperone();
}
}

あるいは、実行時に自動的な親モデルの hydration を有効にするには、リレーションシップを eager loading する際に `chaperone` メソッドを呼び出すことができます。

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

多対一(ポリモーフィック)

モデルには多くの関連モデルがある場合がありますが、「最新」または「最古」の関連モデルを簡単に取得したい場合があります。たとえば、Userモデルは多くのImageモデルに関連付けられている場合がありますが、ユーザーがアップロードした最新の画像とやり取りするための便利な方法を定義したい場合があります。これは、ofManyメソッドと組み合わせてmorphOneリレーションシップタイプを使用することで実現できます。

/**
* Get the user's most recent image.
*/
public function latestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->latestOfMany();
}

同様に、「最古」または最初の関連モデルを取得するメソッドを定義することもできます。

/**
* Get the user's oldest image.
*/
public function oldestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->oldestOfMany();
}

デフォルトでは、 `latestOfMany` メソッドと `oldestOfMany` メソッドは、ソート可能なモデルの主キーに基づいて最新または最古の関連モデルを取得します。ただし、場合によっては、異なるソート基準を使用して、より大きなリレーションシップから単一のモデルを取得したい場合があります。

たとえば、ofManyメソッドを使用して、ユーザーの「最もいいね」された画像を取得できます。ofManyメソッドは、ソート可能な列を最初の引数として受け取り、関連モデルのクエリに適用する集計関数(minまたはmax)を受け取ります。

/**
* Get the user's most popular image.
*/
public function bestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');
}
lightbulb

より高度な「多対一」リレーションシップを構築することは可能です。詳細については、has one of manyドキュメントを参照してください。

多対多(ポリモーフィック)

テーブル構造

多対多のポリモーフィックリレーションシップは、「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
lightbulb

ポリモーフィック多対多のリレーションシップについて詳しく説明する前に、一般的な多対多のリレーションシップに関するドキュメントを読むと役立つ場合があります。

モデル構造

次に、モデルのリレーションシップを定義する準備ができました。PostモデルとVideoモデルには、Eloquentモデルベースクラスによって提供されるmorphToManyメソッドを呼び出すtagsメソッドが含まれます。

morphToManyメソッドは、関連モデルの名前と「リレーションシップ名」を受け取ります。中間テーブル名とそれに含まれるキーに割り当てた名前に基づいて、リレーションシップを「taggable」と呼びます。

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
 
class Post extends Model
{
/**
* Get all of the tags for the post.
*/
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
{
/**
* Get all of the posts that are assigned this tag.
*/
public function posts(): MorphToMany
{
return $this->morphedByMany(Post::class, 'taggable');
}
 
/**
* Get all of the videos that are assigned this tag.
*/
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を呼び出すメソッドの名前をアクセスします。この場合は、Tagモデルのpostsメソッドまたはvideosメソッドです。

use App\Models\Tag;
 
$tag = Tag::find(1);
 
foreach ($tag->posts as $post) {
// ...
}
 
foreach ($tag->videos as $video) {
// ...
}

カスタムポリモーフィックタイプ

デフォルトでは、Laravel は関連モデルの「type」を格納するために完全修飾クラス名を使用します。例えば、上記の1対多のリレーションシップの例では、CommentモデルはPostモデルまたはVideoモデルのどちらかに属している可能性がありますが、デフォルトのcommentable_typeはそれぞれApp\Models\PostまたはApp\Models\Videoになります。ただし、これらの値をアプリケーションの内部構造から切り離したい場合があります。

例えば、モデル名ではなくpostvideoのような単純な文字列を「type」として使用できます。これにより、モデル名が変更されても、データベース内の多態的「type」列の値は有効なままです。

use Illuminate\Database\Eloquent\Relations\Relation;
 
Relation::enforceMorphMap([
'post' => 'App\Models\Post',
'video' => 'App\Models\Video',
]);

App\Providers\AppServiceProviderクラスのbootメソッドでenforceMorphMapメソッドを呼び出すか、必要に応じて別のサービスプロバイダを作成できます。

特定のモデルのモーフエイリアスは、モデルのgetMorphClassメソッドを使用して実行時に決定できます。逆に、モーフエイリアスに関連付けられた完全修飾クラス名は、Relation::getMorphedModelメソッドを使用して決定できます。

use Illuminate\Database\Eloquent\Relations\Relation;
 
$alias = $post->getMorphClass();
 
$class = Relation::getMorphedModel($alias);
exclamation

既存のアプリケーションに「モーフマップ」を追加する場合、完全修飾クラス名を含むデータベース内のすべてのモーフ可能な*_type列の値をその「マップ」名に変換する必要があります。

動的リレーションシップ

resolveRelationUsingメソッドを使用して、Eloquentモデル間のリレーションシップを実行時に定義できます。通常のアプリケーション開発では推奨されませんが、Laravelパッケージの開発時には便利になる場合があります。

resolveRelationUsingメソッドは、最初の引数として目的のリレーションシップ名を受け取ります。メソッドに渡される2番目の引数は、モデルインスタンスを受け取り、有効なEloquentリレーションシップ定義を返すクロージャである必要があります。通常、動的なリレーションシップは、サービスプロバイダbootメソッド内で設定する必要があります。

use App\Models\Order;
use App\Models\Customer;
 
Order::resolveRelationUsing('customer', function (Order $orderModel) {
return $orderModel->belongsTo(Customer::class, 'customer_id');
});
exclamation

動的なリレーションシップを定義する際には、常にEloquentリレーションシップメソッドに明示的なキー名引数を指定してください。

リレーションのクエリ

すべてのEloquentリレーションシップはメソッドによって定義されているため、関連モデルをロードするためのクエリを実行せずに、これらのメソッドを呼び出してリレーションシップのインスタンスを取得できます。さらに、すべてのタイプのEloquentリレーションシップはクエリビルダーとしても機能するため、データベースに対してSQLクエリを実行する前に、リレーションシップクエリに制約をチェーンし続けることができます。

例えば、Userモデルに多くの関連するPostモデルがあるブログアプリケーションを考えてみましょう。

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
 
class User extends Model
{
/**
* Get all of the posts for the user.
*/
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のクエリビルダーのメソッドをリレーションシップで使用できますので、利用可能なすべてのメソッドについてクエリビルダーのドキュメントを参照してください。

リレーションシップ後の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リレーションシップクエリに追加の制約を追加する必要がない場合は、プロパティのようにリレーションシップにアクセスできます。例えば、UserPostの例モデルを引き続き使用して、ユーザーのすべての投稿に次のようにアクセスできます。

use App\Models\User;
 
$user = User::find(1);
 
foreach ($user->posts as $post) {
// ...
}

動的なリレーションシッププロパティは「遅延読み込み」を実行します。つまり、実際にアクセスするまでリレーションシップデータを読み込みません。このため、開発者はしばしば eager loadingを使用して、モデルのロード後にアクセスされることがわかっているリレーションシップを事前にロードします。 eager loading は、モデルのリレーションをロードするために実行する必要があるSQLクエリの数を大幅に削減します。

リレーションの存在クエリ

モデルレコードを取得する際に、リレーションシップの存在に基づいて結果を制限したい場合があります。例えば、少なくとも1つのコメントがあるブログ投稿をすべて取得したいとします。そのためには、リレーションシップの名前をhasメソッドとorHasメソッドに渡します。

use App\Models\Post;
 
// Retrieve all posts that have at least one comment...
$posts = Post::has('comments')->get();

演算子とカウント値を指定して、クエリをさらにカスタマイズすることもできます。

// Retrieve all posts that have three or more comments...
$posts = Post::has('comments', '>=', 3)->get();

ネストされたhasステートメントは、「ドット」表記を使用して構築できます。例えば、少なくとも1つの画像があるコメントが少なくとも1つあるすべての投稿を取得できます。

// Retrieve posts that have at least one comment with images...
$posts = Post::has('comments.images')->get();

さらに強力な機能が必要な場合は、whereHasメソッドとorWhereHasメソッドを使用して、コメントの内容を検査するなど、hasクエリに追加のクエリ制約を定義できます。

use Illuminate\Database\Eloquent\Builder;
 
// Retrieve posts with at least one comment containing words like code%...
$posts = Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
})->get();
 
// Retrieve posts with at least ten comments containing words like code%...
$posts = Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
}, '>=', 10)->get();
exclamation

Eloquentは現在、データベース間でのリレーションシップの存在に関するクエリをサポートしていません。リレーションシップは同じデータベース内に存在する必要があります。

インラインリレーションシップ存在クエリ

リレーションシップクエリに添付された単一の単純なwhere条件でリレーションシップの存在をクエリしたい場合は、whereRelationorWhereRelationwhereMorphRelationorWhereMorphRelationメソッドを使用する方が便利な場合があります。例えば、承認されていないコメントがあるすべての投稿をクエリできます。

use App\Models\Post;
 
$posts = Post::whereRelation('comments', 'is_approved', false)->get();

もちろん、クエリビルダーのwhereメソッドへの呼び出しと同様に、演算子を指定することもできます。

$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リレーションのクエリ

「モーフto」リレーションシップの存在をクエリするには、whereHasMorphメソッドとwhereDoesntHaveMorphメソッドを使用できます。これらのメソッドは、最初の引数としてリレーションシップの名前を受け取ります。次に、メソッドはクエリに含める関連モデルの名前を受け取ります。最後に、リレーションシップクエリをカスタマイズするクロージャを提供できます。

use App\Models\Comment;
use App\Models\Post;
use App\Models\Video;
use Illuminate\Database\Eloquent\Builder;
 
// Retrieve comments associated to posts or videos with a title like code%...
$comments = Comment::whereHasMorph(
'commentable',
[Post::class, Video::class],
function (Builder $query) {
$query->where('title', 'like', 'code%');
}
)->get();
 
// Retrieve comments associated to posts with a title not like code%...
$comments = Comment::whereDoesntHaveMorph(
'commentable',
Post::class,
function (Builder $query) {
$query->where('title', 'like', 'code%');
}
)->get();

関連する多態的モデルの「type」に基づいてクエリ制約を追加する必要がある場合があります。whereHasMorphメソッドに渡されるクロージャは、2番目の引数として$type値を受け取ることができます。この引数を使用すると、構築されているクエリの「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();

「モーフto」リレーションシップの親の子をクエリしたい場合があります。これは、whereMorphedToメソッドとwhereNotMorphedToメソッドを使用して行うことができます。これらのメソッドは、指定されたモデルの適切なモーフタイプマッピングを自動的に決定します。これらのメソッドは、最初の引数としてmorphToリレーションシップの名前、2番目の引数として関連する親モデルを受け取ります。

$comments = Comment::whereMorphedTo('commentable', $post)
->orWhereMorphedTo('commentable', $video)
->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');

カウントクエリに追加のクエリ制約を設定する必要がある場合は、カウントするリレーションシップをキーとする配列を渡します。配列の値は、クエリビルダーインスタンスを受け取るクロージャである必要があります。

$book->loadCount(['reviews' => function (Builder $query) {
$query->where('rating', 5);
}])

リレーションシップカウントとカスタムSELECT文

withCountselect文と組み合わせる場合は、selectメソッドの後にwithCountを呼び出すようにしてください。

$posts = Post::select(['title', 'body'])
->withCount('comments')
->get();

その他の集計関数

withCountメソッドに加えて、EloquentはwithMinwithMaxwithAvgwithSumwithExistsメソッドを提供します。これらのメソッドは、結果のモデルに{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();

「モーフto」リレーションシップを eager load し、そのリレーションシップによって返される可能性のあるさまざまなエンティティの関連モデルカウントも eager load したい場合は、withメソッドとmorphToリレーションシップのmorphWithCountメソッドを組み合わせて使用できます。

この例では、PhotoモデルとPostモデルがActivityFeedモデルを作成できると仮定します。ActivityFeedモデルは、parentableという名前の「モーフto」リレーションシップを定義し、特定のActivityFeedインスタンスの親PhotoまたはPostモデルを取得できるようにすると仮定します。さらに、Photoモデルは多くのTagモデルを持ち、Postモデルは多くのCommentモデルを持つと仮定します。

ここで、ActivityFeedインスタンスを取得し、各ActivityFeedインスタンスのparentable親モデルを eager load するとします。さらに、各親写真に関連付けられているタグの数と、各親投稿に関連付けられているコメントの数も取得したいとします。

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'],
]);

Eager Loading

Eloquentのリレーションシップをプロパティとしてアクセスする場合、関連モデルは「遅延ロード」されます。これは、プロパティに初めてアクセスするまで、リレーションシップデータが実際にロードされないことを意味します。しかし、Eloquentでは、親モデルをクエリする際にリレーションシップを「早期ロード」することができます。早期ロードは「N + 1」クエリ問題を軽減します。「N + 1」クエリ問題を説明するために、Authorモデルに「属する」Bookモデルを考えてみましょう。

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Book extends Model
{
/**
* Get the author that wrote the book.
*/
public function author(): BelongsTo
{
return $this->belongsTo(Author::class);
}
}

では、すべての書籍とその著者を取得してみましょう。

use App\Models\Book;
 
$books = Book::all();
 
foreach ($books as $book) {
echo $book->author->name;
}

このループは、データベーステーブル内のすべての書籍を取得するために1つのクエリを実行し、次に書籍の著者を取得するために各書籍ごとに別のクエリを実行します。したがって、25冊の本がある場合、上記のコードは26個のクエリを実行します。最初の書籍のクエリと、各書籍の著者を取得するための追加の25個のクエリです。

ありがたいことに、早期ロードを使用してこの操作をわずか2つのクエリに減らすことができます。クエリを作成する際に、withメソッドを使用して、どのリレーションシップを早期ロードするかを指定できます。

$books = Book::with('author')->get();
 
foreach ($books as $book) {
echo $book->author->name;
}

この操作では、2つのクエリしか実行されません。1つはすべての書籍を取得するためのクエリ、もう1つはすべての書籍のすべての著者を取得するためのクエリです。

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リレーションシップと、そのリレーションシップによって返される可能性のあるさまざまなエンティティ上のネストされたリレーションシップを早期ロードしたい場合は、morphToリレーションシップのmorphWithメソッドと組み合わせてwithメソッドを使用できます。このメソッドを説明するために、次のモデルを考えてみましょう。

<?php
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
 
class ActivityFeed extends Model
{
/**
* Get the parent of the activity feed record.
*/
public function parentable(): MorphTo
{
return $this->morphTo();
}
}

この例では、EventPhotoPostモデルが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();
exclamation

この機能を使用する場合は、常にid列と関連する外部キー列を、取得する列のリストに含める必要があります。

デフォルトでの早期ロード

モデルを取得する際に、常にいくつかのリレーションシップをロードしたい場合があります。これを実現するには、モデルに$withプロパティを定義できます。

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Book extends Model
{
/**
* The relationships that should always be loaded.
*
* @var array
*/
protected $with = ['author'];
 
/**
* Get the author that wrote the book.
*/
public function author(): BelongsTo
{
return $this->belongsTo(Author::class);
}
 
/**
* Get the genre of the book.
*/
public function genre(): BelongsTo
{
return $this->belongsTo(Genre::class);
}
}

単一のクエリに対して$withプロパティからアイテムを削除したい場合は、withoutメソッドを使用できます。

$books = Book::without('author')->get();

単一のクエリに対して$withプロパティ内のすべてのアイテムを上書きしたい場合は、withOnlyメソッドを使用できます。

$books = Book::withOnly('genre')->get();

Eager Loadの制約

リレーションシップを早期ロードし、早期ロードクエリに追加のクエリ条件を指定したい場合があります。これは、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」という単語が含まれている投稿のみを早期ロードします。早期ロード操作をさらにカスタマイズするために、他のクエリビルダーメソッドを呼び出すことができます。

$users = User::with(['posts' => function (Builder $query) {
$query->orderBy('created_at', 'desc');
}])->get();

morphToリレーションシップの早期ロードの制約

morphToリレーションシップを早期ロードする場合、Eloquentは複数のクエリを実行して、各タイプの関連モデルを取得します。MorphToリレーションのconstrainメソッドを使用して、これらのクエリのそれぞれに追加の制約を追加できます。

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」のビデオのみを早期ロードします。

リレーションシップの存在による早期ロードの制約

リレーションシップの存在を確認し、同時に同じ条件に基づいてリレーションシップをロードする必要がある場合があります。たとえば、指定されたクエリ条件に一致する子Postモデルを持つUserモデルのみを取得し、一致する投稿を早期ロードする場合があります。これは、withWhereHasメソッドを使用して実現できます。

use App\Models\User;
 
$users = User::withWhereHas('posts', function ($query) {
$query->where('featured', true);
})->get();

Lazy Eager Loading

親モデルが既に取得された後に、リレーションシップを早期ロードする必要がある場合があります。たとえば、関連モデルをロードするかどうかを動的に決定する必要がある場合に便利です。

use App\Models\Book;
 
$books = Book::all();
 
if ($someCondition) {
$books->load('author', 'publisher');
}

早期ロードクエリに追加のクエリ制約を設定する必要がある場合は、ロードするリレーションシップをキーとする配列を渡すことができます。配列の値は、クエリインスタンスを受け取るクロージャインスタンスである必要があります。

$author->load(['books' => function (Builder $query) {
$query->orderBy('published_date', 'asc');
}]);

リレーションシップがまだロードされていない場合にのみロードするには、loadMissingメソッドを使用します。

$book->loadMissing('author');

ネストされた遅延早期ロードとmorphTo

morphToリレーションシップと、そのリレーションシップによって返される可能性のあるさまざまなエンティティ上のネストされたリレーションシップを早期ロードしたい場合は、loadMorphメソッドを使用できます。

このメソッドは、最初の引数としてmorphToリレーションシップの名前を受け取り、2番目の引数としてモデル/リレーションシップのペアの配列を受け取ります。このメソッドを説明するために、次のモデルを考えてみましょう。

<?php
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
 
class ActivityFeed extends Model
{
/**
* Get the parent of the activity feed record.
*/
public function parentable(): MorphTo
{
return $this->morphTo();
}
}

この例では、EventPhotoPostモデルがActivityFeedモデルを作成できると仮定します。さらに、EventモデルはCalendarモデルに属し、PhotoモデルはTagモデルに関連付けられ、PostモデルはAuthorモデルに属すると仮定します。

これらのモデル定義とリレーションシップを使用して、ActivityFeedモデルインスタンスを取得し、すべてのparentableモデルとそのそれぞれのネストされたリレーションシップを早期ロードできます。

$activities = ActivityFeed::with('parentable')
->get()
->loadMorph('parentable', [
Event::class => ['calendar'],
Photo::class => ['tags'],
Post::class => ['author'],
]);

Lazy Loadingの防止

前述のように、リレーションシップを早期ロードすると、アプリケーションのパフォーマンスが大幅に向上することがよくあります。したがって、必要に応じて、リレーションシップの遅延ロードを常に防止するようにLaravelに指示できます。これを実現するには、基本的なEloquentモデルクラスで提供されるpreventLazyLoadingメソッドを呼び出すことができます。通常、これはアプリケーションのAppServiceProviderクラスのbootメソッド内で呼び出す必要があります。

preventLazyLoadingメソッドは、遅延ロードを防止するかどうかを示すオプションのブール値引数を受け取ります。たとえば、非本番環境でのみ遅延ロードを無効にし、本番環境が遅延ロードされたリレーションシップが本番コードに誤って存在する場合でも正常に機能し続けるようにすることができます。

use Illuminate\Database\Eloquent\Model;
 
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}

遅延ロードを防止した後、アプリケーションがEloquentリレーションシップを遅延ロードしようとすると、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は、リレーションシップに新しいモデルを追加するための便利なメソッドを提供します。たとえば、投稿に新しいコメントを追加する必要があるとします。Commentモデルにpost_id属性を手動で設定する代わりに、リレーションシップの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メソッドは、新しいCommentモデルに適切なpost_id値を自動的に追加します。

複数の関連モデルを保存する必要がある場合は、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();
 
// All comments, including the newly saved comment...
$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メソッドも使用できます。savecreateの違いは、saveが完全なEloquentモデルインスタンスを受け取るのに対し、createはプレーンなPHParrayを受け取ることです。新しく作成されたモデルは、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.'],
]);

findOrNewfirstOrNewfirstOrCreateupdateOrCreateメソッドを使用して、リレーションシップでモデルを作成および更新することもできます。

lightbulb

createメソッドを使用する前に、一括代入のドキュメントを確認してください。

Belongs Toリレーションシップ

子モデルを新しい親モデルに割り当てる場合は、associateメソッドを使用できます。この例では、UserモデルはAccountモデルへのbelongsToリレーションシップを定義しています。このassociateメソッドは、子モデルの外部キーを設定します。

use App\Models\Account;
 
$account = Account::find(10);
 
$user->account()->associate($account);
 
$user->save();

親モデルを子モデルから削除するには、dissociateメソッドを使用できます。このメソッドは、リレーションシップの外部キーをnullに設定します。

$user->account()->dissociate();
 
$user->save();

多対多リレーションシップ

アタッチ/デタッチ

Eloquentは、多対多のリレーションシップの処理をより便利にするメソッドも提供します。たとえば、ユーザーが複数のロールを持つことができ、ロールが複数のユーザーを持つことができるとします。attachメソッドを使用して、リレーションシップの中間テーブルにレコードを挿入することで、ロールをユーザーにアタッチできます。

use App\Models\User;
 
$user = User::find(1);
 
$user->roles()->attach($roleId);

モデルにリレーションシップを追加する際、中間テーブルに挿入する追加データの配列を渡すこともできます。

$user->roles()->attach($roleId, ['expires' => $expires]);

ユーザーからロールを削除する必要がある場合があります。多対多のリレーションシップレコードを削除するには、detachメソッドを使用します。detachメソッドは中間テーブルから適切なレコードを削除しますが、両方のモデルはデータベースに残ります。

// Detach a single role from the user...
$user->roles()->detach($roleId);
 
// Detach all roles from the user...
$user->roles()->detach();

便宜上、attachdetachはIDの配列も入力として受け付けます。

$user = User::find(1);
 
$user->roles()->detach([1, 2, 3]);
 
$user->roles()->attach([
1 => ['expires' => $expires],
2 => ['expires' => $expires],
]);

関連付けの同期

syncメソッドを使用して多対多の関連付けを作成することもできます。syncメソッドは、中間テーブルに配置するIDの配列を受け取ります。指定された配列にないIDは、中間テーブルから削除されます。そのため、この操作が完了した後、指定された配列内のIDのみが中間テーブルに存在します。

$user->roles()->sync([1, 2, 3]);

IDと共に追加の中間テーブルの値を渡すこともできます。

$user->roles()->sync([1 => ['expires' => true], 2, 3]);

同期されたモデルIDごとに同じ中間テーブルの値を挿入する場合は、syncWithPivotValuesメソッドを使用できます。

$user->roles()->syncWithPivotValues([1, 2, 3], ['active' => true]);

指定された配列にない既存のIDを削除しない場合は、syncWithoutDetachingメソッドを使用できます。

$user->roles()->syncWithoutDetaching([1, 2, 3]);

関連付けの切り替え

多対多のリレーションシップは、指定された関連モデルIDの添付状態を「切り替える」toggleメソッドも提供します。指定された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リレーションシップを定義する場合(例:Postに属するComment)、子モデルが更新されたときに親のタイムスタンプを更新することが役立つ場合があります。

たとえば、Commentモデルが更新されたときに、所有するPostupdated_atタイムスタンプを自動的に「タッチ」して、現在の日時になるように設定できます。これを実現するには、子モデルにtouchesプロパティを追加し、子モデルが更新されたときにupdated_atタイムスタンプを更新する必要があるリレーションシップの名前を含めます。

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Comment extends Model
{
/**
* All of the relationships to be touched.
*
* @var array
*/
protected $touches = ['post'];
 
/**
* Get the post that the comment belongs to.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}
exclamation

親モデルのタイムスタンプは、Eloquentのsaveメソッドを使用して子モデルが更新された場合にのみ更新されます。