本文へ移動
Laravel Tips

リレーション(表どうしのつながり)

表どうしのつながり(リレーション)の作り方を、1対1・1対多・多対多・ポリモーフィックまで説明し、検索・数え方・先読み・保存のしかたもまとめます。

データベースの表は、たがいにつながっていることがよくあります。たとえば、ブログの記事には複数のコメントが付き、注文には注文した人がいます。このつながりをリレーション(表どうしの関係)といいます。Eloquent(Laravel のモデルのしくみ)を使うと、このつながりを PHP のメソッドとして書き、$post->comments のように簡単にたどれます。

Eloquent が扱えるリレーションの種類は、次のとおりです。

種類 説明
1対1(hasOne) 1つのモデルに、もう1つのモデルが1つだけ結びつく
1対多(hasMany) 1つのモデルに、複数のモデルが結びつく
多対多(belongsToMany) たがいに複数と結びつく。間に中間の表を使う
Has One Through 間にもう1つのモデルをはさんで、1つのモデルにたどりつく
Has Many Through 間にもう1つのモデルをはさんで、複数のモデルにたどりつく
1対1(ポリモーフィック) 子が、複数の種類の親のどれか1つに結びつく(親は1つ)
1対多(ポリモーフィック) 子が、複数の種類の親のどれかに結びつく(親に子が複数)
多対多(ポリモーフィック) 複数の種類のモデルが、たがいに複数と結びつく

リレーションの書き方#

リレーションは、モデルのクラスの中のメソッド(クラスの中の関数)として書きます。リレーションはクエリビルダ(SQL を書かずにデータベースへ問い合わせるしくみ)としても使えるので、あとに条件をつなげられます。

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

1対1(Has One)#

1対1は、いちばん基本のつながりです。たとえば、User(ユーザー)に Phone(電話番号)が1つ結びつく場合、User モデルに phone メソッドを作り、hasOne の結果を返します。

php
<?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 の第1引数(1番目に渡す値)は、結びつく相手のモデルのクラス名です。定義したあとは、動的プロパティ(メソッドを、まるでプロパティのように書いて呼ぶ書き方)で取り出せます。

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

Eloquent は、親のモデルの名前から外部キー(ほかの表の行を指す列)を決めます。この例では、Phone の表に user_id という列があるものとして探します。名前がちがうときは、第2引数で指定します。

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

また、外部キーの値は、親の主キー(id の列)と照らし合わせます。id 以外の列と照らしたいときは、第3引数で指定します。

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

逆向きのつながり(Belongs To)#

User から Phone をたどれるようになりました。逆に、Phone から持ち主の User をたどるには、belongsTo を使います。

php
<?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 は Phone の user_id 列と同じ id を持つ User を探します。外部キーの名前は、メソッドの名前のうしろに _id を付けて決めます。名前がちがうときは、第2引数で指定します。

php
public function user(): BelongsTo
{
    return $this->belongsTo(User::class, 'foreign_key');
}

親の表が id 以外の列で探されるときは、第3引数で親の列を指定します。

php
public function user(): BelongsTo
{
    return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
}

1対多(Has Many)#

1対多は、1つのモデルが、複数の子のモデルの親になるつながりです。たとえば、ブログの記事には、いくつでもコメントが付きます。

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

外部キーは、親のモデルの名前を「スネークケース」(小文字を _ でつなぐ書き方)にして、_id を付けたものになります。この例では、Comment の表の post_id です。

動的プロパティの comments で、コメントのコレクション(配列を便利に扱う入れ物)が取れます。

php
use App\Models\Post;

$comments = Post::find(1)->comments;

foreach ($comments as $comment) {
    // ...
}

リレーションはクエリビルダでもあるので、comments() とメソッドで呼んで、条件をつなげられます。

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

hasOne と同じように、外部キーと親側の列の名前も指定できます。

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

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

子から親を自動で取り出す(chaperone)#

先読み(あとで説明します)をしていても、子のループの中で親をたどると、「N + 1」問題(問い合わせの数がむだに増える問題)が起きることがあります。

php
$posts = Post::with('comments')->get();

foreach ($posts as $post) {
    foreach ($post->comments as $comment) {
        echo $comment->post->title;
    }
}

コメントを先読みしても、各コメントに親の Post は自動では入らないからです。入れてほしいときは、hasMany を定義するときに chaperone を呼びます。

php
public function comments(): HasMany
{
    return $this->hasMany(Comment::class)->chaperone();
}

使うときだけ有効にしたいなら、先読みのときに chaperone を呼びます。

php
use App\Models\Post;

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

1対多の逆向き(Belongs To)#

コメントから親の記事をたどるには、子のモデルに belongsTo のメソッドを作ります。

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

動的プロパティで親が取れます。

php
use App\Models\Comment;

$comment = Comment::find(1);

return $comment->post->title;

Eloquent は、Comment の post_id と同じ id を持つ Post を探します。外部キーの名前は、メソッド名のうしろに _ と親の主キーの列名を付けて決めます。名前がちがうときは、第2引数・第3引数で指定します。

php
public function post(): BelongsTo
{
    return $this->belongsTo(Post::class, 'foreign_key');
}

public function post(): BelongsTo
{
    return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');
}

空のときの代わりのモデル(Default Models)#

belongsTo・hasOne・hasOneThrough・morphOne では、相手がいないとき(null)に返す「代わりのモデル」を決められます。null かどうかをいちいち調べなくて済みます(Null Object パターンと呼ばれる考え方です)。

php
public function user(): BelongsTo
{
    return $this->belongsTo(User::class)->withDefault();
}

代わりのモデルに値を入れたいときは、配列かクロージャ(名前のない関数)を渡します。

php
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';
    });
}

Belongs To で子を探す#

「このユーザーの記事」を探すとき、where を自分で書いてもよいです。

php
use App\Models\Post;

$posts = Post::where('user_id', $user->id)->get();

whereBelongsTo を使うと、リレーションと外部キーを自動で決めてくれます。

php
$posts = Post::whereBelongsTo($user)->get();

コレクションを渡すと、そのどれかに属するモデルが取れます。

php
$users = User::where('vip', true)->get();

$posts = Post::whereBelongsTo($users)->get();

リレーションの名前は、モデルのクラス名から決まります。ちがう名前にしたいときは、第2引数で指定します。

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

Has One of Many(多くの中から1つ)#

多くの関連モデルのうち、「いちばん新しい」「いちばん古い」1つだけを取り出したいことがあります。たとえば、ユーザーの注文のうち、最新の1件です。hasOne と ofMany 系のメソッドを組み合わせます。

php
public function latestOrder(): HasOne
{
    return $this->hasOne(Order::class)->latestOfMany();
}

いちばん古いものは oldestOfMany です。

php
public function oldestOrder(): HasOne
{
    return $this->hasOne(Order::class)->oldestOfMany();
}

latestOfMany と oldestOfMany は、ふつう主キーの順で決めます(並べ替えられる列であることが必要です)。ほかの基準で選びたいときは ofMany を使います。第1引数に列、第2引数に集計の関数(min か max)を渡します。

php
public function largestOrder(): HasOne
{
    return $this->hasOne(Order::class)->ofMany('price', 'max');
}

注意

PostgreSQL は UUID(重ならないように作る長い ID)の列に MAX を使えません。そのため、UUID の列と PostgreSQL を組み合わせた Has One of Many は、いまのところ使えません。

「多」のリレーションを「1」に変える#

すでに hasMany を定義しているなら、one を呼ぶと、同じ内容から「1つだけ」のリレーションを作れます。

php
public function orders(): HasMany
{
    return $this->hasMany(Order::class);
}

public function largestOrder(): HasOne
{
    return $this->orders()->one()->ofMany('price', 'max');
}

HasManyThrough を HasOneThrough に変えるときも、one を使えます。

php
public function latestDeployment(): HasOneThrough
{
    return $this->deployments()->one()->latestOfMany();
}

もっと細かい条件の Has One of Many#

たとえば、商品(Product)に、価格(Price)がたくさん残っていて、未来の日付で先に登録しておける場合を考えます。ほしいのは「公開日が未来ではない、いちばん新しい価格」です。公開日が同じなら、id が大きいほうを選びます。

ofMany に、並べ替えの列を配列で渡し、第2引数のクロージャで公開日の条件を足します。

php
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(間をはさんで1つ)#

間に別のモデルをはさんで、遠くのモデルを1つ取り出すつながりです。

たとえば、車の修理工場で、整備士(Mechanic)は車(Car)を1台、車は持ち主(Owner)を1人持つとします。整備士と持ち主は表の上では直接つながっていませんが、車を通して持ち主をたどれます。表は次のとおりです。

text
mechanics
    id - integer
    name - string

cars
    id - integer
    model - string
    mechanic_id - integer

owners
    id - integer
    name - string
    car_id - integer

Mechanic モデルに、次のように書きます。

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

第1引数は、最後にたどりつきたいモデル、第2引数は、間のモデルです。

すべてのモデルにリレーションがもう定義してあるなら、through を使って書けます。たとえば、Mechanic に cars、Car に owner があるときです。

php
// 文字で書く方法
return $this->through('cars')->has('owner');

// メソッド名で書く方法
return $this->throughCars()->hasOwner();

キーの名前を変える#

外部キーの名前は、ふつうの決まりで決まります。変えたいときは、hasOneThrough の引数で指定します。

引数 意味
第3引数 間のモデルの外部キー
第4引数 最後のモデルの外部キー
第5引数 最初のモデルの、照らし合わせる列
第6引数 間のモデルの、照らし合わせる列
php
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 の表の照らし合わせる列
        );
    }
}

先ほどの through を使う書き方なら、すでに定義したリレーションのキーの決まりをそのまま使えるのが利点です。

php
// 文字で書く方法
return $this->through('cars')->has('owner');

// メソッド名で書く方法
return $this->throughCars()->hasOwner();

Has Many Through(間をはさんで複数)#

間に別のモデルをはさんで、遠くの複数のモデルを取り出すつながりです。

たとえば、アプリを公開するサービス(Laravel Cloud のようなもの)を作るとします。Application(アプリ)は、Environment(環境)を通して、たくさんの Deployment(公開の記録)を持ちます。アプリのすべての公開の記録をまとめて取れます。表は次のとおりです。

text
applications
    id - integer
    name - string

environments
    id - integer
    application_id - integer
    name - string

deployments
    id - integer
    environment_id - integer
    commit_hash - string

Application モデルに、次のように書きます。

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;

class Application extends Model
{
    /**
     * アプリのすべての公開の記録を取り出す
     */
    public function deployments(): HasManyThrough
    {
        return $this->hasManyThrough(Deployment::class, Environment::class);
    }
}

第1引数は最後にたどりつきたいモデル、第2引数は間のモデルです。

すべてのモデルにリレーションが定義済みなら、through でも書けます。

php
// 文字で書く方法
return $this->through('environments')->has('deployments');

// メソッド名で書く方法
return $this->throughEnvironments()->hasDeployments();

Deployment の表には application_id の列がありません。それでも $application->deployments で取れるのは、Eloquent がまず間の Environment の表の application_id を見て環境の id を集め、その id で Deployment の表を調べているからです。

キーの名前を変える#

キーの名前を変える引数は、hasOneThrough と同じ並びです(第3引数が間のモデルの外部キー、第4引数が最後のモデルの外部キー、第5引数が最初のモデルの列、第6引数が間のモデルの列)。

php
class Application extends Model
{
    public function deployments(): HasManyThrough
    {
        return $this->hasManyThrough(
            Deployment::class,
            Environment::class,
            'application_id', // environments の表の外部キー
            'environment_id', // deployments の表の外部キー
            'id', // applications の表の照らし合わせる列
            'id' // environments の表の照らし合わせる列
        );
    }
}

through を使う書き方なら、定義済みのリレーションのキーの決まりを再利用できます。

php
// 文字で書く方法
return $this->through('environments')->has('deployments');

// メソッド名で書く方法
return $this->throughEnvironments()->hasDeployments();

条件つきのリレーション(Scoped Relationships)#

リレーションに条件を足したメソッドを、モデルに作ることはよくあります。たとえば、posts に条件を足した featuredPosts です。

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

    /**
     * ユーザーの注目の記事を取り出す
     */
    public function featuredPosts(): HasMany
    {
        return $this->posts()->where('featured', true);
    }
}

ただし、この featuredPosts から記事を作っても、featured は true になりません。作るときにも同じ値を入れたいなら、withAttributes を使います。

php
public function featuredPosts(): HasMany
{
    return $this->posts()->withAttributes(['featured' => true]);
}

withAttributes は、検索の条件(where)を足し、さらにこのリレーションで作るモデルにも値を入れます。

php
$post = $user->featuredPosts()->create(['title' => 'Featured Post']);

$post->featured; // true

検索の条件を足したくないときは、asConditions を false にします。

php
return $this->posts()->withAttributes(['featured' => true], asConditions: false);

多対多(Many to Many)#

多対多は、hasOne や hasMany より少し複雑です。たとえば、ユーザーが複数の「役割」(ロール)を持ち、同じ役割を別のユーザーも持てる場合です。「著者」と「編集者」の役割を持つユーザーがいて、その役割はほかのユーザーにも付けられます。

表の作り#

表は3つ必要です。users、roles、そして中間の表 role_user です。中間の表の名前は、2つのモデルの名前をアルファベット順に並べて決まり、user_id と role_id の列を持ちます。

役割は複数のユーザーに付くので、roles の表に user_id を置くことはできません(1人のユーザーにしか付けられなくなるため)。そこで中間の表を使います。

text
users
    id - integer
    name - string

roles
    id - integer
    name - string

role_user
    user_id - integer
    role_id - integer

モデルの作り#

belongsToMany の結果を返すメソッドを書きます。User モデルに roles メソッドを作ります。第1引数は、相手のモデルです。

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

定義したら、動的プロパティで取り出せます。

php
use App\Models\User;

$user = User::find(1);

foreach ($user->roles as $role) {
    // ...
}

ほかのリレーションと同じく、メソッドで呼んで条件をつなげることもできます。

php
$roles = User::find(1)->roles()->orderBy('name')->get();

中間の表の名前を変えたいときは、第2引数で指定します。

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

中間の表の列の名前も変えられます。第3引数は、このモデル側の外部キー、第4引数は、相手のモデル側の外部キーです。

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

逆向きのつながり#

逆向きも、相手のモデルに belongsToMany のメソッドを作ります。Role モデルに users を作ります。

php
<?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 側と同じ書き方で、中間の表やキーの名前を変える引数も、そのまま使えます。

中間の表の列を取り出す#

中間の表は、モデルの pivot という属性で取り出せます。

php
use App\Models\User;

$user = User::find(1);

foreach ($user->roles as $role) {
    echo $role->pivot->created_at;
}

取り出した Role モデルには、自動で pivot が付きます。中身は、中間の表を表すモデルです。

最初は、モデルのキーだけが pivot に入ります。中間の表にほかの列があるなら、リレーションを書くときに withPivot で指定します。

php
return $this->belongsToMany(Role::class)->withPivot('active', 'created_by');

中間の表の created_at と updated_at を Eloquent に自動で管理させたいなら、withTimestamps を呼びます。

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

注意

Eloquent に時刻を自動で管理させる中間の表には、created_at と updated_at の両方の列が必要です。

pivot の名前を変える#

pivot という名前は、役割に合わせて変えられます。たとえば、ユーザーがポッドキャストを購読するなら、subscription のほうが分かりやすいです。リレーションを書くときに as を使います。

php
return $this->belongsToMany(Podcast::class)
    ->as('subscription')
    ->withTimestamps();

変えたあとは、新しい名前で取り出せます。

php
$users = User::with('podcasts')->get();

foreach ($users->flatMap->podcasts as $podcast) {
    echo $podcast->subscription->created_at;
}

中間の表の列で絞り込む#

リレーションを書くときに、次のメソッドで中間の表の列を条件にできます。

メソッド 説明
wherePivot 中間の表の列が、指定した値のものに絞る
wherePivotIn 列の値が、指定した配列の中にあるものに絞る
wherePivotNotIn 列の値が、指定した配列の中にないものに絞る
wherePivotBetween 列の値が、指定した範囲の中にあるものに絞る
wherePivotNotBetween 列の値が、指定した範囲の外にあるものに絞る
wherePivotNull 列の値が null のものに絞る
wherePivotNotNull 列の値が null ではないものに絞る
php
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');

wherePivot は検索の条件を足すだけで、このリレーションで新しく作るときには値を入れません。検索にも作成にも同じ値を使いたいなら、withPivotValue を使います。

php
return $this->belongsToMany(Role::class)
    ->withPivotValue('approved', 1);

中間の表の列で並べ替える#

orderByPivot と orderByPivotDesc で、中間の表の列を使って並べ替えられます。次の例は、ユーザーのバッジを、新しい順に取り出します。

php
return $this->belongsToMany(Badge::class)
    ->where('rank', 'gold')
    ->orderByPivotDesc('created_at');

中間の表に専用のモデルを使う#

中間の表を表す専用のモデルを作りたいときは、リレーションを書くときに using を呼びます。専用のモデルには、メソッドやキャスト(型の変換)を足せます。

中間モデルは、決まったクラスを継承して(もとにして)作ります。多対多なら Illuminate\Database\Eloquent\Relations\Pivot、ポリモーフィックの多対多なら Illuminate\Database\Eloquent\Relations\MorphPivot です。例として、Role が RoleUser という中間モデルを使う場合です。

php
<?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 は Pivot を継承します。

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Relations\Pivot;

class RoleUser extends Pivot
{
    // ...
}

注意

中間モデルでは、SoftDeletes(消したことにして残すしくみ)のトレイト(クラスに機能を足す部品)は使えません。中間の行を消したことにして残したいなら、中間モデルを本物の Eloquent モデルに作り直すことを考えてください。

中間モデルと自動で増える ID#

専用の中間モデルが、自動で増える主キーを持つ場合は、中間モデルに Table 属性を付けて、incrementing を true にします。

php
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Relations\Pivot;

#[Table(incrementing: true)]
class RoleUser extends Pivot
{
    // ...
}

中間モデルのリレーションを自動で入れる#

専用の中間モデルに、両側のモデルへの belongsTo を定義してあるなら、chaperone を呼ぶと、そのリレーションを各中間モデルに自動で入れます。中間モデルから相手をたどるときの、むだな問い合わせが減ります。

php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\Pivot;

class RoleUser extends Pivot
{
    public function role(): BelongsTo
    {
        return $this->belongsTo(Role::class);
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

class Role extends Model
{
    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class)
            ->using(RoleUser::class)
            ->chaperone();
    }
}

Eloquent は、中間モデルのリレーションの名前を推測します。ふつうとちがう名前なら、chaperone に名前を渡します。

php
return $this->belongsToMany(User::class)
    ->using(RoleUser::class)
    ->chaperone(declaring: 'role', related: 'user');

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

ポリモーフィック(「いろいろな形」という意味)なリレーションは、子のモデルが、1つのつながりで、複数の種類の親のどれかに属せるしくみです。たとえば、ユーザーが記事と動画を共有できるアプリで、Comment(コメント)を Post(記事)にも Video(動画)にも付けたいときに使います。

1対1(ポリモーフィック)#

通常の1対1に似ていますが、子が複数の種類の親に属せます。たとえば、Post と User が、どちらも Image(画像)を1つ持てる場合です。画像の表を1つにまとめられます。

表の作り#

text
posts
    id - integer
    name - string

users
    id - integer
    name - string

images
    id - integer
    url - string
    imageable_type - string
    imageable_id - integer

images の表の imageable_id と imageable_type に注目してください。imageable_id には、記事やユーザーの id が入ります。imageable_type には、親のモデルのクラス名が入ります。Eloquent はこの列を見て、どの種類の親を返すかを決めます。ここには App\Models\Post か App\Models\User が入ります。

モデルの作り#

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class Image extends Model
{
    /**
     * 親のモデル(ユーザーか記事)を取り出す
     */
    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 で取れます。

php
use App\Models\Post;

$post = Post::find(1);

$image = $post->image;

画像の親は、morphTo を呼んでいるメソッドの名前(ここでは imageable)で取れます。

php
use App\Models\Image;

$image = Image::find(1);

$imageable = $image->imageable;

imageable は、画像の持ち主に応じて、Post か User のどちらかを返します。

キーの名前を変える#

「id」と「type」の列の名前を変えたいときは、morphTo の第1引数にリレーションの名前を必ず渡します。ふつうはメソッド名と同じなので、PHP の __FUNCTION__(いまの関数名が入る定数)が使えます。

php
public function imageable(): MorphTo
{
    return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');
}

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

通常の1対多に似ていますが、子が複数の種類の親に属せます。たとえば、記事にも動画にもコメントできるとします。コメントの表を1つにまとめられます。

表の作り#

text
posts
    id - integer
    title - string
    body - text

videos
    id - integer
    title - string
    url - string

comments
    id - integer
    body - text
    commentable_type - string
    commentable_id - integer

モデルの作り#

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class Comment extends Model
{
    /**
     * 親のモデル(記事か動画)を取り出す
     */
    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 で取れます。

php
use App\Models\Post;

$post = Post::find(1);

foreach ($post->comments as $comment) {
    // ...
}

コメントの親は、morphTo を呼んでいるメソッド(ここでは commentable)で取れます。

php
use App\Models\Comment;

$comment = Comment::find(1);

$commentable = $comment->commentable;

commentable は、親に応じて、Post か Video のどちらかを返します。

子から親を自動で取り出す#

通常の1対多と同じく、先読みしても、子のループの中で親をたどると「N + 1」問題が起きます。

php
$posts = Post::with('comments')->get();

foreach ($posts as $post) {
    foreach ($post->comments as $comment) {
        echo $comment->commentable->title;
    }
}

親を子に自動で入れたいときは、morphMany を定義するときに chaperone を呼びます。

php
class Post extends Model
{
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable')->chaperone();
    }
}

使うときだけにしたいなら、先読みのときに chaperone を呼びます。

php
use App\Models\Post;

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

多くの中から1つ(ポリモーフィック)#

ポリモーフィックでも、「最新」「最古」の1つだけを取り出せます。たとえば、ユーザーがアップロードした画像のうち、いちばん新しい1枚です。morphOne と ofMany 系のメソッドを組み合わせます。

php
public function latestImage(): MorphOne
{
    return $this->morphOne(Image::class, 'imageable')->latestOfMany();
}

いちばん古いものは oldestOfMany です。

php
public function oldestImage(): MorphOne
{
    return $this->morphOne(Image::class, 'imageable')->oldestOfMany();
}

ふつうは主キーの順で決まります。ほかの基準(たとえば「いいね」がいちばん多い画像)なら ofMany を使います。

php
public function bestImage(): MorphOne
{
    return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');
}

補足

もっと細かい条件の「多くの中から1つ」も作れます。前の「もっと細かい条件の Has One of Many」を見てください。

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

ポリモーフィックの多対多は、「morph one」「morph many」より少し複雑です。たとえば、Post と Video が、どちらも Tag(タグ)を共有できる場合です。タグの表を1つにまとめ、記事にも動画にも付けられます。

表の作り#

text
posts
    id - integer
    name - string

videos
    id - integer
    name - string

tags
    id - integer
    name - string

taggables
    tag_id - integer
    taggable_type - string
    taggable_id - integer

補足

ポリモーフィックの多対多に進む前に、ふつうの多対多(前の節)を読んでおくと分かりやすくなります。

モデルの作り#

Post と Video の両方に、morphToMany を呼ぶ tags メソッドを作ります。

morphToMany には、相手のモデルと「リレーションの名前」を渡します。中間の表の名前とキーの名前から、ここでは taggable と呼びます。

php
<?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
<?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 で取れます。

php
use App\Models\Post;

$post = Post::find(1);

foreach ($post->tags as $tag) {
    // ...
}

タグから親をたどるには、Tag モデルの posts や videos を使います。

php
use App\Models\Tag;

$tag = Tag::find(1);

foreach ($tag->posts as $post) {
    // ...
}

foreach ($tag->videos as $video) {
    // ...
}

type の値を決める(Custom Polymorphic Types)#

ふつう Laravel は、「type」の列に、クラス名(App\Models\Post など)をそのまま保存します。しかし、アプリの内部の作りに、データベースの値を結びつけたくないことがあります。

たとえば、クラス名の代わりに post や video のような短い文字を使うと、モデルの名前を変えても、データベースの値は有効なままです。enforceMorphMap で対応表(morph map)を登録します。

php
use Illuminate\Database\Eloquent\Relations\Relation;

Relation::enforceMorphMap([
    'post' => 'App\Models\Post',
    'video' => 'App\Models\Video',
]);

enforceMorphMap は、App\Providers\AppServiceProvider の boot メソッドで呼びます。専用のサービスプロバイダ(アプリの起動のときに道具を登録する場所)を作って呼んでもかまいません。

モデルの別名は getMorphClass で、別名からクラス名は Relation::getMorphedModel で取れます。

php
use Illuminate\Database\Eloquent\Relations\Relation;

$alias = $post->getMorphClass();

$class = Relation::getMorphedModel($alias);

注意

すでに動いているアプリに対応表を足すときは、データベースの *_type の列に入っているクラス名を、すべて対応表の名前に書き換える必要があります。

実行中にリレーションを足す(Dynamic Relationships)#

resolveRelationUsing で、実行中にモデルどうしのリレーションを定義できます。ふつうのアプリ作りでは、あまりおすすめしません。パッケージ(ほかのアプリでも使える部品のまとまり)を作るときに役立つことがあります。

第1引数がリレーションの名前、第2引数が、モデルを受け取ってリレーションを返すクロージャです。ふつうは、サービスプロバイダの boot メソッドで設定します。

php
use App\Models\Order;
use App\Models\Customer;

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

注意

実行中にリレーションを定義するときは、リレーションのメソッドに、キーの名前を必ず明示して渡してください。

リレーションで検索する#

リレーションはメソッドなので、呼んでも、すぐには検索が走りません。メソッドで呼んでリレーションを手に入れ、クエリビルダの条件をつなげてから、実行できます。

例として、User が多くの Post を持つとします。

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

条件を足して検索できます。

php
use App\Models\User;

$user = User::find(1);

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

クエリビルダのメソッドは、どれもリレーションに使えます。

orWhere をつなげるときの注意#

リレーションに orWhere をつなげるときは注意が必要です。orWhere は、リレーションの条件と同じ階層に置かれてしまいます。

php
$user->posts()
    ->where('active', 1)
    ->orWhere('votes', '>=', 100)
    ->get();

上の書き方は、次の SQL(データベースへの命令)になります。or のせいで、特定のユーザーに絞られず、「100票以上のすべての記事」が返ってしまいます。

sql
select *
from posts
where user_id = ? and active = 1 or votes >= 100

ほとんどの場合は、条件をグループにして、かっこでくくります。

php
use Illuminate\Database\Eloquent\Builder;

$user->posts()
    ->where(function (Builder $query) {
        return $query->where('active', 1)
            ->orWhere('votes', '>=', 100);
    })
    ->get();

こうすると、次の SQL になり、特定のユーザーに絞られたままになります。

sql
select *
from posts
where user_id = ? and (active = 1 or votes >= 100)

メソッドと動的プロパティのちがい#

リレーションに条件を足す必要がなければ、プロパティのように書けます。

php
use App\Models\User;

$user = User::find(1);

foreach ($user->posts as $post) {
    // ...
}

動的プロパティは遅延読み込み(lazy loading。実際に使った瞬間に読み込むこと)をします。そのため、あとで使うと分かっているなら、先読み(eager loading)で、先にまとめて読み込むのがおすすめです。実行する SQL の数が大きく減ります。

リレーションがあるものを探す#

「コメントが1つ以上ある記事」のように、リレーションがあるかどうかで絞りたいときは、has と orHas を使います。

php
use App\Models\Post;

// コメントが1つ以上ある記事
$posts = Post::has('comments')->get();

演算子(>= のような比べる記号)と数も指定できます。

php
// コメントが3つ以上ある記事
$posts = Post::has('comments', '>=', 3)->get();

ドット(.)で、入れ子のリレーションも指定できます。

php
// 画像つきのコメントが1つ以上ある記事
$posts = Post::has('comments.images')->get();

もっと細かい条件には、whereHas と orWhereHas を使います。たとえば、コメントの中身を調べられます。

php
use Illuminate\Database\Eloquent\Builder;

// 「code」で始まるコメントが1つ以上ある記事
$posts = Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
})->get();

// 「code」で始まるコメントが10個以上ある記事
$posts = Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
}, '>=', 10)->get();

注意

Eloquent は、別々のデータベースにまたがるリレーションの存在の検索に、いまのところ対応していません。リレーションは、同じデータベースの中にある必要があります。

多対多のリレーションがあるものを探す#

whereAttachedTo は、あるモデル(またはモデルのコレクション)と多対多で結びついているモデルを探します。

php
$users = User::whereAttachedTo($role)->get();

コレクションを渡すと、そのどれかに結びついているモデルが取れます。

php
$tags = Tag::whereLike('name', '%laravel%')->get();

$posts = Post::whereAttachedTo($tags)->get();

1行で書く検索#

リレーションに簡単な条件を1つ付けるだけなら、whereRelation・orWhereRelation・whereMorphRelation・orWhereMorphRelation が便利です。たとえば、承認されていないコメントがある記事です。

php
use App\Models\Post;

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

where と同じく、演算子も指定できます。

php
$posts = Post::whereRelation(
    'comments', 'created_at', '>=', now()->minus(hours: 1)
)->get();

リレーションがないものを探す#

「コメントが1つもない記事」のように、リレーションがないものを探すには、doesntHave と orDoesntHave を使います。

php
use App\Models\Post;

$posts = Post::doesntHave('comments')->get();

もっと細かい条件には、whereDoesntHave と orWhereDoesntHave を使います。

php
use Illuminate\Database\Eloquent\Builder;

$posts = Post::whereDoesntHave('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
})->get();

ドット(.)で、入れ子のリレーションにも使えます。次の例は、コメントのない記事と、コメントはあっても、どれも追放されたユーザーのものではない記事を取り出します。

php
use Illuminate\Database\Eloquent\Builder;

$posts = Post::whereDoesntHave('comments.author', function (Builder $query) {
    $query->where('banned', 1);
})->get();

Morph To のリレーションで探す#

morphTo のリレーションを探すには、whereHasMorph と whereDoesntHaveMorph を使います。第1引数はリレーションの名前、第2引数は対象のモデル、第3引数は条件を決めるクロージャです。

php
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();

親の種類(type)によって条件を変えたいときは、クロージャの第2引数で $type を受け取れます。

php
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();

「ある親モデルの子」を探したいときは、whereMorphedTo と whereNotMorphedTo が使えます。type の対応は自動で決まります。第1引数に morphTo のリレーションの名前、第2引数に親のモデルを渡します。

php
$comments = Comment::whereMorphedTo('commentable', $post)
    ->orWhereMorphedTo('commentable', $video)
    ->get();

すべての種類を対象にする#

モデルの配列の代わりに * を渡すと、データベースにあるすべての種類を対象にします。そのために、Laravel は追加の問い合わせを1回実行します。

php
use Illuminate\Database\Eloquent\Builder;

$comments = Comment::whereHasMorph('commentable', '*', function (Builder $query) {
    $query->where('title', 'like', 'foo%');
})->get();

関連するモデルを集計する#

数える(withCount)#

関連するモデルを読み込まずに、数だけ知りたいときは withCount を使います。結果のモデルに {リレーション名}_count という属性が付きます。

php
use App\Models\Post;

$posts = Post::withCount('comments')->get();

foreach ($posts as $post) {
    echo $post->comments_count;
}

配列を渡すと、複数のリレーションを数えたり、条件を足したりできます。

php
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;

別名を付ければ、同じリレーションを条件を変えて何度も数えられます。

php
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)#

親のモデルを取り出したあとで数えたいときは、loadCount を使います。

php
$book = Book::first();

$book->loadCount('genres');

条件を足したいときは、リレーション名をキーにした配列を渡し、値にクロージャを書きます。

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

select と組み合わせるとき#

withCount を select と組み合わせるなら、select のあとで withCount を呼びます。

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

ほかの集計のメソッド#

withCount のほかに、次のメソッドがあります。結果には {リレーション名}_{関数}_{列名} という属性が付きます。

メソッド 説明
withMin 関連するモデルの、列の最小値
withMax 関連するモデルの、列の最大値
withAvg 関連するモデルの、列の平均
withSum 関連するモデルの、列の合計
withExists 関連するモデルが存在するか(はい・いいえ)
php
use App\Models\Post;

$posts = Post::withSum('comments', 'votes')->get();

foreach ($posts as $post) {
    echo $post->comments_sum_votes;
}

別の名前で取りたいときは、別名を付けます。

php
$posts = Post::withSum('comments as total_comments', 'votes')->get();

foreach ($posts as $post) {
    echo $post->total_comments;
}

loadCount と同じく、あとから実行する版(loadSum など)もあります。

php
$post = Post::first();

$post->loadSum('comments', 'votes');

select と組み合わせるときは、これらも select のあとで呼びます。

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

Morph To のリレーションで数える#

morphTo のリレーションを先読みしつつ、返ってきたモデルごとに関連の数も知りたいときは、with と、morphTo の morphWithCount を組み合わせます。

たとえば、Photo と Post が ActivityFeed を作れるとします。ActivityFeed には、親の Photo か Post を取れる parentable という morphTo があります。Photo は多くの Tag を、Post は多くの Comment を持ちます。各親について、写真のタグの数と記事のコメントの数を取りたい場合です。

php
use Illuminate\Database\Eloquent\Relations\MorphTo;

$activities = ActivityFeed::with([
    'parentable' => function (MorphTo $morphTo) {
        $morphTo->morphWithCount([
            Photo::class => ['tags'],
            Post::class => ['comments'],
        ]);
    }])->get();

あとから数える(loadMorphCount)#

すでに ActivityFeed を取り出したあとで、parentable の関連の数を読み込むには、loadMorphCount を使います。

php
$activities = ActivityFeed::with('parentable')->get();

$activities->loadMorphCount('parentable', [
    Photo::class => ['tags'],
    Post::class => ['comments'],
]);

先読み(Eager Loading)#

リレーションをプロパティとして使うと、必要になった瞬間に読み込む遅延読み込みになります。これに対し、親のモデルを取り出すときに、いっしょに読み込んでおくのが先読みです。先読みは「N + 1」問題を防ぎます。

Book(本)が Author(著者)に属する例で考えましょう。

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

すべての本と著者を取り出します。

php
use App\Models\Book;

$books = Book::all();

foreach ($books as $book) {
    echo $book->author->name;
}

このループは、本を取るのに1回、さらに各本の著者を取るのに1回ずつ、問い合わせを実行します。本が25冊なら、1 + 25 で、26回です。

先読みを使うと、2回で済みます。with で、先読みするリレーションを指定します。

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

foreach ($books as $book) {
    echo $book->author->name;
}

実行されるのは、本を取る1回と、その著者をまとめて取る1回だけです。

sql
select * from books

select * from authors where id in (1, 2, 3, 4, 5, ...)

複数のリレーションを先読みする#

配列で渡します。

php
$books = Book::with(['author', 'publisher'])->get();

入れ子のリレーションを先読みする#

ドット(.)で、リレーションのリレーションも先読みできます。

php
$books = Book::with('author.contacts')->get();

入れ子の配列でも書けます。

php
$books = Book::with([
    'author' => [
        'contacts',
        'publisher',
    ],
])->get();

morphTo の入れ子を先読みする#

morphTo を先読みしつつ、返ってきた各モデルの入れ子のリレーションも先読みしたいときは、with と morphTo の morphWith を組み合わせます。次のモデルで考えます。

php
<?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 に属します。すべての parentable と、その入れ子のリレーションを先読みします。

php
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();

取り出す列を絞る#

リレーションの全部の列が要らないときは、列を指定できます。

php
$books = Book::with('author:id,name,book_id')->get();

注意

列を絞るときは、id の列と、関係する外部キーの列を、必ず含めてください。

いつも先読みする#

いつも読み込みたいリレーションは、モデルの $with プロパティに書きます。

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

1回の検索だけ $with の一部を外したいときは、without を使います。

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

1回の検索だけ $with をまるごと置き換えたいときは、withOnly を使います。

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

先読みに条件を足す#

先読みの検索にも条件を足せます。リレーション名をキー、条件を足すクロージャを値にした配列を、with に渡します。

php
use App\Models\User;

$users = User::with(['posts' => function ($query) {
    $query->where('title', 'like', '%code%');
}])->get();

この例では、題名に code を含む記事だけを先読みします。ほかのクエリビルダのメソッドも使えます。

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

morphTo の先読みに条件を足す#

morphTo を先読みすると、Eloquent は種類ごとに別々の問い合わせを実行します。種類ごとの条件は、MorphTo の constrain で足せます。

php
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();

この例では、隠されていない記事と、type が educational の動画だけを先読みします。

リレーションの存在と先読みを同時にする#

「条件に合う記事を持つユーザーだけを取り出し、その記事も先読みする」ような、存在の確認と先読みを同じ条件でしたいときは、withWhereHas を使います。

php
use App\Models\User;

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

あとから先読みする(Lazy Eager Loading)#

親のモデルを取り出したあとで、先読みしたくなることもあります。たとえば、読み込むかどうかを条件で決めたいときです。

php
use App\Models\Book;

$books = Book::all();

if ($condition) {
    $books->load('author', 'publisher');
}

条件を足したいときは、リレーション名をキー、クロージャを値にした配列を渡します。

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

まだ読み込んでいないときだけ読み込むには、loadMissing を使います。

php
$book->loadMissing('author');

morphTo の入れ子をあとから先読みする#

morphTo と、返ってきた各モデルの入れ子のリレーションを、あとから先読みするには、loadMorph を使います。第1引数に morphTo のリレーション名、第2引数に、モデルとリレーションの組の配列を渡します。例には、先ほどの ActivityFeed を使います。

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

自動で先読みする(Automatic Eager Loading)#

多くの場合、Laravel は、使ったリレーションを自動で先読みできます。有効にするには、AppServiceProvider の boot メソッドで Model::automaticallyEagerLoadRelationships を呼びます。

php
use Illuminate\Database\Eloquent\Model;

/**
 * アプリの起動時の処理
 */
public function boot(): void
{
    Model::automaticallyEagerLoadRelationships();
}

有効にすると、まだ読み込んでいないリレーションを使った瞬間に、Laravel が自動で読み込みます。次の例を見てください。

php
use App\Models\User;

$users = User::all();

foreach ($users as $user) {
    foreach ($user->posts as $post) {
        foreach ($post->comments as $comment) {
            echo $comment->content;
        }
    }
}

ふつうなら、ユーザーごとに記事の問い合わせ、記事ごとにコメントの問い合わせが走ります。自動の先読みを有効にすると、どれか1人のユーザーの posts を使った瞬間に、取り出したすべてのユーザーの posts をまとめて読み込みます。コメントも同じです。

全体では有効にしたくないときは、1つの Eloquent コレクションだけに、withRelationshipAutoloading で有効にできます。

php
$users = User::where('vip', true)->get();

return $users->withRelationshipAutoloading();

遅延読み込みを禁止する(Preventing Lazy Loading)#

先読みは、アプリを速くします。そこで、遅延読み込みを禁止することもできます。Model::preventLazyLoading を、AppServiceProvider の boot メソッドで呼びます。

引数に真偽値(true か false)を渡せます。たとえば、本番以外でだけ禁止すれば、うっかり遅延読み込みが残っていても、本番は止まらずに動きます。

php
use Illuminate\Database\Eloquent\Model;

/**
 * アプリの起動時の処理
 */
public function boot(): void
{
    Model::preventLazyLoading(! $this->app->isProduction());
}

禁止したあとに遅延読み込みをすると、Eloquent は Illuminate\Database\LazyLoadingViolationException という例外(エラーを知らせるしくみ)を投げます。

handleLazyLoadingViolationUsing で、違反したときの動きを変えられます。たとえば、例外にせず、ログに記録するだけにできます。

php
Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {
    $class = $model::class;

    info("Attempted to lazy load [{$relation}] on model [{$class}].");
});

関連するモデルを保存・更新する#

save メソッド#

リレーションを通して、新しいモデルを足せます。たとえば、記事にコメントを足すとき、post_id を自分で入れなくても、リレーションの save が入れてくれます。

php
use App\Models\Comment;
use App\Models\Post;

$comment = new Comment(['message' => 'A new comment.']);

$post = Post::find(1);

$post->comments()->save($comment);

ここでは、comments を動的プロパティではなく、メソッドで呼んでいる点に注意してください(リレーションそのものが必要だからです)。

複数をまとめて保存するときは saveMany です。

php
$post = Post::find(1);

$post->comments()->saveMany([
    new Comment(['message' => 'A new comment.']),
    new Comment(['message' => 'Another new comment.']),
]);

save と saveMany は、モデルを保存しますが、親にすでに読み込んであるリレーションには、新しいモデルを足しません。保存したあとにリレーションを使うなら、refresh で読み込み直します。

php
$post->comments()->save($comment);

$post->refresh();

// 新しく保存したコメントを含む、すべてのコメント
$post->comments;

まとめて保存する(push)#

モデルと、そのリレーションをすべて保存したいときは、push を使います。次の例では、記事と、そのコメント、コメントの著者が保存されます。

php
$post = Post::find(1);

$post->comments[0]->message = 'Message';
$post->comments[0]->author->name = 'Author Name';

$post->push();

pushQuietly は、イベント(「〜が起きた」という知らせ)を出さずに保存します。

php
$post->pushQuietly();

create メソッド#

create は、配列を受け取り、モデルを作って保存します。save は完成したモデルを受け取り、create は普通の配列を受け取る点がちがいます。作ったモデルが返ります。

php
use App\Models\Post;

$post = Post::find(1);

$comment = $post->comments()->create([
    'message' => 'A new comment.',
]);

複数まとめて作るときは createMany です。

php
$post = Post::find(1);

$post->comments()->createMany([
    ['message' => 'A new comment.'],
    ['message' => 'Another new comment.'],
]);

createQuietly と createManyQuietly は、イベントを出さずに作ります。

php
$user = User::find(1);

$user->posts()->createQuietly([
    'title' => 'Post title.',
]);

$user->posts()->createManyQuietly([
    ['title' => 'First post.'],
    ['title' => 'Second post.'],
]);

findOrNew・firstOrNew・firstOrCreate・updateOrCreate も、リレーションで使えます(Eloquent の基本を見てください)。

補足

create を使う前に、マスアサインメント(配列でまとめて値を入れること)の説明を読んでおいてください。Eloquent の基本にあります。

Belongs To の更新#

子のモデルを、別の親に付け替えるには、associate を使います。次の例は、User が Account に属する場合です。associate は、子の外部キーを設定します。

php
use App\Models\Account;

$account = Account::find(10);

$user->account()->associate($account);

$user->save();

親との結びつきを外すには、dissociate を使います。外部キーが null になります。

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

$user->save();

多対多の更新#

結びつける・外す(attach / detach)#

多対多では、attach で、中間の表に行を足して結びつけます。たとえば、ユーザーに役割を付けます。

php
use App\Models\User;

$user = User::find(1);

$user->roles()->attach($roleId);

中間の表に入れる追加の値を、配列で渡せます。

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

結びつきを外すには detach を使います。中間の表の行だけが消え、2つのモデルはデータベースに残ります。

php
// ユーザーから、1つの役割を外す
$user->roles()->detach($roleId);

// ユーザーから、すべての役割を外す
$user->roles()->detach();

attach と detach は、ID の配列も受け取れます。

php
$user = User::find(1);

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

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

そろえる(sync)#

sync は、ID の配列を受け取り、中間の表をその内容にそろえます。配列にない ID は、中間の表から外されます。終わったあとは、配列の ID だけが残ります。

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

ID といっしょに、中間の表の値も渡せます。

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

同じ中間の表の値を、すべての ID に入れたいときは、syncWithPivotValues を使います。

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

配列にない既存の ID を外したくないときは、syncWithoutDetaching を使います。

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

入れ替える(toggle)#

toggle は、渡した ID の結びつきを「入れ替え」ます。結びついていれば外し、結びついていなければ結びつけます。

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

中間の表の値も渡せます。

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

トランザクションで動かす#

ここまでの中間の表の操作には、OrFail が付いた版があります(attachOrFail・detachOrFail・syncOrFail・syncWithoutDetachingOrFail・toggleOrFail)。トランザクション(まとめて成功するか、まとめて取り消すかにするしくみ)の中で実行され、例外が起きると、変更はすべて元に戻ります。

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

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

中間の表の行を更新する#

中間の表に、すでにある行を更新するには、updateExistingPivot を使います。相手のモデルの ID と、更新する値の配列を渡します。

php
$user = User::find(1);

$user->roles()->updateExistingPivot($roleId, [
    'active' => false,
]);

親の更新日時を更新する(touch)#

belongsTo や belongsToMany で親に属しているとき、子が更新されたら、親の更新日時も更新したいことがあります。たとえば、Comment が更新されたら、親の Post の updated_at を現在の日時にしたい場合です。

子のモデルに Touches 属性を付けて、更新日時を更新したいリレーションの名前を並べます。

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Touches;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

#[Touches(['post'])]
class Comment extends Model
{
    /**
     * コメントの記事を取り出す
     */
    public function post(): BelongsTo
    {
        return $this->belongsTo(Post::class);
    }
}

注意

親の更新日時が更新されるのは、子のモデルを Eloquent の save メソッドで更新したときだけです。

関連するページ#

公式ドキュメント(英語)

2026年10月5日時点の内容をもとに、日本語でまとめています。

ページの一覧