リレーション(表どうしのつながり)
表どうしのつながり(リレーション)の作り方を、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 を書かずにデータベースへ問い合わせるしくみ)としても使えるので、あとに条件をつなげられます。
$user->posts()->where('active', 1)->get();
1対1(Has One)#
1対1は、いちばん基本のつながりです。たとえば、User(ユーザー)に Phone(電話番号)が1つ結びつく場合、User モデルに phone メソッドを作り、hasOne の結果を返します。
<?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番目に渡す値)は、結びつく相手のモデルのクラス名です。定義したあとは、動的プロパティ(メソッドを、まるでプロパティのように書いて呼ぶ書き方)で取り出せます。
$phone = User::find(1)->phone;
Eloquent は、親のモデルの名前から外部キー(ほかの表の行を指す列)を決めます。この例では、Phone の表に user_id という列があるものとして探します。名前がちがうときは、第2引数で指定します。
return $this->hasOne(Phone::class, 'foreign_key');
また、外部キーの値は、親の主キー(id の列)と照らし合わせます。id 以外の列と照らしたいときは、第3引数で指定します。
return $this->hasOne(Phone::class, 'foreign_key', 'local_key');
逆向きのつながり(Belongs To)#
User から Phone をたどれるようになりました。逆に、Phone から持ち主の User をたどるには、belongsTo を使います。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Phone extends Model
{
/**
* この電話番号の持ち主を取り出す
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
user を呼ぶと、Eloquent は Phone の user_id 列と同じ id を持つ User を探します。外部キーの名前は、メソッドの名前のうしろに _id を付けて決めます。名前がちがうときは、第2引数で指定します。
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key');
}
親の表が id 以外の列で探されるときは、第3引数で親の列を指定します。
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
}
1対多(Has Many)#
1対多は、1つのモデルが、複数の子のモデルの親になるつながりです。たとえば、ブログの記事には、いくつでもコメントが付きます。
<?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 で、コメントのコレクション(配列を便利に扱う入れ物)が取れます。
use App\Models\Post;
$comments = Post::find(1)->comments;
foreach ($comments as $comment) {
// ...
}
リレーションはクエリビルダでもあるので、comments() とメソッドで呼んで、条件をつなげられます。
$comment = Post::find(1)->comments()
->where('title', 'foo')
->first();
hasOne と同じように、外部キーと親側の列の名前も指定できます。
return $this->hasMany(Comment::class, 'foreign_key');
return $this->hasMany(Comment::class, 'foreign_key', 'local_key');
子から親を自動で取り出す(chaperone)#
先読み(あとで説明します)をしていても、子のループの中で親をたどると、「N + 1」問題(問い合わせの数がむだに増える問題)が起きることがあります。
$posts = Post::with('comments')->get();
foreach ($posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->post->title;
}
}
コメントを先読みしても、各コメントに親の Post は自動では入らないからです。入れてほしいときは、hasMany を定義するときに chaperone を呼びます。
public function comments(): HasMany
{
return $this->hasMany(Comment::class)->chaperone();
}
使うときだけ有効にしたいなら、先読みのときに chaperone を呼びます。
use App\Models\Post;
$posts = Post::with([
'comments' => fn ($comments) => $comments->chaperone(),
])->get();
1対多の逆向き(Belongs To)#
コメントから親の記事をたどるには、子のモデルに belongsTo のメソッドを作ります。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Comment extends Model
{
/**
* コメントの記事を取り出す
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}
動的プロパティで親が取れます。
use App\Models\Comment;
$comment = Comment::find(1);
return $comment->post->title;
Eloquent は、Comment の post_id と同じ id を持つ Post を探します。外部キーの名前は、メソッド名のうしろに _ と親の主キーの列名を付けて決めます。名前がちがうときは、第2引数・第3引数で指定します。
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 パターンと呼ばれる考え方です)。
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault();
}
代わりのモデルに値を入れたいときは、配列かクロージャ(名前のない関数)を渡します。
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault([
'name' => 'Guest Author',
]);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault(function (User $user, Post $post) {
$user->name = 'Guest Author';
});
}
Belongs To で子を探す#
「このユーザーの記事」を探すとき、where を自分で書いてもよいです。
use App\Models\Post;
$posts = Post::where('user_id', $user->id)->get();
whereBelongsTo を使うと、リレーションと外部キーを自動で決めてくれます。
$posts = Post::whereBelongsTo($user)->get();
コレクションを渡すと、そのどれかに属するモデルが取れます。
$users = User::where('vip', true)->get();
$posts = Post::whereBelongsTo($users)->get();
リレーションの名前は、モデルのクラス名から決まります。ちがう名前にしたいときは、第2引数で指定します。
$posts = Post::whereBelongsTo($user, 'author')->get();
Has One of Many(多くの中から1つ)#
多くの関連モデルのうち、「いちばん新しい」「いちばん古い」1つだけを取り出したいことがあります。たとえば、ユーザーの注文のうち、最新の1件です。hasOne と ofMany 系のメソッドを組み合わせます。
public function latestOrder(): HasOne
{
return $this->hasOne(Order::class)->latestOfMany();
}
いちばん古いものは oldestOfMany です。
public function oldestOrder(): HasOne
{
return $this->hasOne(Order::class)->oldestOfMany();
}
latestOfMany と oldestOfMany は、ふつう主キーの順で決めます(並べ替えられる列であることが必要です)。ほかの基準で選びたいときは ofMany を使います。第1引数に列、第2引数に集計の関数(min か max)を渡します。
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つだけ」のリレーションを作れます。
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
public function largestOrder(): HasOne
{
return $this->orders()->one()->ofMany('price', 'max');
}
HasManyThrough を HasOneThrough に変えるときも、one を使えます。
public function latestDeployment(): HasOneThrough
{
return $this->deployments()->one()->latestOfMany();
}
もっと細かい条件の Has One of Many#
たとえば、商品(Product)に、価格(Price)がたくさん残っていて、未来の日付で先に登録しておける場合を考えます。ほしいのは「公開日が未来ではない、いちばん新しい価格」です。公開日が同じなら、id が大きいほうを選びます。
ofMany に、並べ替えの列を配列で渡し、第2引数のクロージャで公開日の条件を足します。
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人持つとします。整備士と持ち主は表の上では直接つながっていませんが、車を通して持ち主をたどれます。表は次のとおりです。
mechanics
id - integer
name - string
cars
id - integer
model - string
mechanic_id - integer
owners
id - integer
name - string
car_id - integer
Mechanic モデルに、次のように書きます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
class Mechanic extends Model
{
/**
* 車の持ち主を取り出す
*/
public function carOwner(): HasOneThrough
{
return $this->hasOneThrough(Owner::class, Car::class);
}
}
第1引数は、最後にたどりつきたいモデル、第2引数は、間のモデルです。
すべてのモデルにリレーションがもう定義してあるなら、through を使って書けます。たとえば、Mechanic に cars、Car に owner があるときです。
// 文字で書く方法
return $this->through('cars')->has('owner');
// メソッド名で書く方法
return $this->throughCars()->hasOwner();
キーの名前を変える#
外部キーの名前は、ふつうの決まりで決まります。変えたいときは、hasOneThrough の引数で指定します。
| 引数 | 意味 |
|---|---|
| 第3引数 | 間のモデルの外部キー |
| 第4引数 | 最後のモデルの外部キー |
| 第5引数 | 最初のモデルの、照らし合わせる列 |
| 第6引数 | 間のモデルの、照らし合わせる列 |
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 を使う書き方なら、すでに定義したリレーションのキーの決まりをそのまま使えるのが利点です。
// 文字で書く方法
return $this->through('cars')->has('owner');
// メソッド名で書く方法
return $this->throughCars()->hasOwner();
Has Many Through(間をはさんで複数)#
間に別のモデルをはさんで、遠くの複数のモデルを取り出すつながりです。
たとえば、アプリを公開するサービス(Laravel Cloud のようなもの)を作るとします。Application(アプリ)は、Environment(環境)を通して、たくさんの Deployment(公開の記録)を持ちます。アプリのすべての公開の記録をまとめて取れます。表は次のとおりです。
applications
id - integer
name - string
environments
id - integer
application_id - integer
name - string
deployments
id - integer
environment_id - integer
commit_hash - string
Application モデルに、次のように書きます。
<?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 でも書けます。
// 文字で書く方法
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引数が間のモデルの列)。
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 を使う書き方なら、定義済みのリレーションのキーの決まりを再利用できます。
// 文字で書く方法
return $this->through('environments')->has('deployments');
// メソッド名で書く方法
return $this->throughEnvironments()->hasDeployments();
条件つきのリレーション(Scoped Relationships)#
リレーションに条件を足したメソッドを、モデルに作ることはよくあります。たとえば、posts に条件を足した featuredPosts です。
<?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 を使います。
public function featuredPosts(): HasMany
{
return $this->posts()->withAttributes(['featured' => true]);
}
withAttributes は、検索の条件(where)を足し、さらにこのリレーションで作るモデルにも値を入れます。
$post = $user->featuredPosts()->create(['title' => 'Featured Post']);
$post->featured; // true
検索の条件を足したくないときは、asConditions を false にします。
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人のユーザーにしか付けられなくなるため)。そこで中間の表を使います。
users
id - integer
name - string
roles
id - integer
name - string
role_user
user_id - integer
role_id - integer
モデルの作り#
belongsToMany の結果を返すメソッドを書きます。User モデルに roles メソッドを作ります。第1引数は、相手のモデルです。
<?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);
}
}
定義したら、動的プロパティで取り出せます。
use App\Models\User;
$user = User::find(1);
foreach ($user->roles as $role) {
// ...
}
ほかのリレーションと同じく、メソッドで呼んで条件をつなげることもできます。
$roles = User::find(1)->roles()->orderBy('name')->get();
中間の表の名前を変えたいときは、第2引数で指定します。
return $this->belongsToMany(Role::class, 'role_user');
中間の表の列の名前も変えられます。第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
{
/**
* この役割を持つユーザーを取り出す
*/
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
}
User 側と同じ書き方で、中間の表やキーの名前を変える引数も、そのまま使えます。
中間の表の列を取り出す#
中間の表は、モデルの pivot という属性で取り出せます。
use App\Models\User;
$user = User::find(1);
foreach ($user->roles as $role) {
echo $role->pivot->created_at;
}
取り出した Role モデルには、自動で pivot が付きます。中身は、中間の表を表すモデルです。
最初は、モデルのキーだけが pivot に入ります。中間の表にほかの列があるなら、リレーションを書くときに withPivot で指定します。
return $this->belongsToMany(Role::class)->withPivot('active', 'created_by');
中間の表の created_at と updated_at を Eloquent に自動で管理させたいなら、withTimestamps を呼びます。
return $this->belongsToMany(Role::class)->withTimestamps();
注意
Eloquent に時刻を自動で管理させる中間の表には、created_at と updated_at の両方の列が必要です。
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;
}
中間の表の列で絞り込む#
リレーションを書くときに、次のメソッドで中間の表の列を条件にできます。
| メソッド | 説明 |
|---|---|
wherePivot |
中間の表の列が、指定した値のものに絞る |
wherePivotIn |
列の値が、指定した配列の中にあるものに絞る |
wherePivotNotIn |
列の値が、指定した配列の中にないものに絞る |
wherePivotBetween |
列の値が、指定した範囲の中にあるものに絞る |
wherePivotNotBetween |
列の値が、指定した範囲の外にあるものに絞る |
wherePivotNull |
列の値が null のものに絞る |
wherePivotNotNull |
列の値が null ではないものに絞る |
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 を使います。
return $this->belongsToMany(Role::class)
->withPivotValue('approved', 1);
中間の表の列で並べ替える#
orderByPivot と orderByPivotDesc で、中間の表の列を使って並べ替えられます。次の例は、ユーザーのバッジを、新しい順に取り出します。
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
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
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
class RoleUser extends Pivot
{
// ...
}
注意
中間モデルでは、SoftDeletes(消したことにして残すしくみ)のトレイト(クラスに機能を足す部品)は使えません。中間の行を消したことにして残したいなら、中間モデルを本物の Eloquent モデルに作り直すことを考えてください。
中間モデルと自動で増える ID#
専用の中間モデルが、自動で増える主キーを持つ場合は、中間モデルに Table 属性を付けて、incrementing を true にします。
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Relations\Pivot;
#[Table(incrementing: true)]
class RoleUser extends Pivot
{
// ...
}
中間モデルのリレーションを自動で入れる#
専用の中間モデルに、両側のモデルへの belongsTo を定義してあるなら、chaperone を呼ぶと、そのリレーションを各中間モデルに自動で入れます。中間モデルから相手をたどるときの、むだな問い合わせが減ります。
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 に名前を渡します。
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つにまとめられます。
表の作り#
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
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 で取れます。
use App\Models\Post;
$post = Post::find(1);
$image = $post->image;
画像の親は、morphTo を呼んでいるメソッドの名前(ここでは imageable)で取れます。
use App\Models\Image;
$image = Image::find(1);
$imageable = $image->imageable;
imageable は、画像の持ち主に応じて、Post か User のどちらかを返します。
キーの名前を変える#
「id」と「type」の列の名前を変えたいときは、morphTo の第1引数にリレーションの名前を必ず渡します。ふつうはメソッド名と同じなので、PHP の __FUNCTION__(いまの関数名が入る定数)が使えます。
public function imageable(): MorphTo
{
return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');
}
1対多(ポリモーフィック)#
通常の1対多に似ていますが、子が複数の種類の親に属せます。たとえば、記事にも動画にもコメントできるとします。コメントの表を1つにまとめられます。
表の作り#
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
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 で取れます。
use App\Models\Post;
$post = Post::find(1);
foreach ($post->comments as $comment) {
// ...
}
コメントの親は、morphTo を呼んでいるメソッド(ここでは commentable)で取れます。
use App\Models\Comment;
$comment = Comment::find(1);
$commentable = $comment->commentable;
commentable は、親に応じて、Post か Video のどちらかを返します。
子から親を自動で取り出す#
通常の1対多と同じく、先読みしても、子のループの中で親をたどると「N + 1」問題が起きます。
$posts = Post::with('comments')->get();
foreach ($posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->commentable->title;
}
}
親を子に自動で入れたいときは、morphMany を定義するときに chaperone を呼びます。
class Post extends Model
{
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable')->chaperone();
}
}
使うときだけにしたいなら、先読みのときに chaperone を呼びます。
use App\Models\Post;
$posts = Post::with([
'comments' => fn ($comments) => $comments->chaperone(),
])->get();
多くの中から1つ(ポリモーフィック)#
ポリモーフィックでも、「最新」「最古」の1つだけを取り出せます。たとえば、ユーザーがアップロードした画像のうち、いちばん新しい1枚です。morphOne と ofMany 系のメソッドを組み合わせます。
public function latestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->latestOfMany();
}
いちばん古いものは oldestOfMany です。
public function oldestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->oldestOfMany();
}
ふつうは主キーの順で決まります。ほかの基準(たとえば「いいね」がいちばん多い画像)なら ofMany を使います。
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つにまとめ、記事にも動画にも付けられます。
表の作り#
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
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
class Post extends Model
{
/**
* 記事のすべてのタグを取り出す
*/
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
}
逆向きのつながり#
Tag モデルには、親になりうるモデルごとにメソッドを作ります。ここでは posts と videos です。どちらも morphedByMany の結果を返します。
morphedByMany にも、相手のモデルと「リレーションの名前」(ここでは taggable)を渡します。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
class Tag extends Model
{
/**
* このタグが付いたすべての記事を取り出す
*/
public function posts(): MorphToMany
{
return $this->morphedByMany(Post::class, 'taggable');
}
/**
* このタグが付いたすべての動画を取り出す
*/
public function videos(): MorphToMany
{
return $this->morphedByMany(Video::class, 'taggable');
}
}
取り出し方#
記事のタグは、動的プロパティ tags で取れます。
use App\Models\Post;
$post = Post::find(1);
foreach ($post->tags as $tag) {
// ...
}
タグから親をたどるには、Tag モデルの posts や videos を使います。
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)を登録します。
use Illuminate\Database\Eloquent\Relations\Relation;
Relation::enforceMorphMap([
'post' => 'App\Models\Post',
'video' => 'App\Models\Video',
]);
enforceMorphMap は、App\Providers\AppServiceProvider の boot メソッドで呼びます。専用のサービスプロバイダ(アプリの起動のときに道具を登録する場所)を作って呼んでもかまいません。
モデルの別名は getMorphClass で、別名からクラス名は Relation::getMorphedModel で取れます。
use Illuminate\Database\Eloquent\Relations\Relation;
$alias = $post->getMorphClass();
$class = Relation::getMorphedModel($alias);
注意
すでに動いているアプリに対応表を足すときは、データベースの *_type の列に入っているクラス名を、すべて対応表の名前に書き換える必要があります。
実行中にリレーションを足す(Dynamic Relationships)#
resolveRelationUsing で、実行中にモデルどうしのリレーションを定義できます。ふつうのアプリ作りでは、あまりおすすめしません。パッケージ(ほかのアプリでも使える部品のまとまり)を作るときに役立つことがあります。
第1引数がリレーションの名前、第2引数が、モデルを受け取ってリレーションを返すクロージャです。ふつうは、サービスプロバイダの boot メソッドで設定します。
use App\Models\Order;
use App\Models\Customer;
Order::resolveRelationUsing('customer', function (Order $orderModel) {
return $orderModel->belongsTo(Customer::class, 'customer_id');
});
注意
実行中にリレーションを定義するときは、リレーションのメソッドに、キーの名前を必ず明示して渡してください。
リレーションで検索する#
リレーションはメソッドなので、呼んでも、すぐには検索が走りません。メソッドで呼んでリレーションを手に入れ、クエリビルダの条件をつなげてから、実行できます。
例として、User が多くの Post を持つとします。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Model
{
/**
* ユーザーのすべての記事を取り出す
*/
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
}
条件を足して検索できます。
use App\Models\User;
$user = User::find(1);
$user->posts()->where('active', 1)->get();
クエリビルダのメソッドは、どれもリレーションに使えます。
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)
メソッドと動的プロパティのちがい#
リレーションに条件を足す必要がなければ、プロパティのように書けます。
use App\Models\User;
$user = User::find(1);
foreach ($user->posts as $post) {
// ...
}
動的プロパティは遅延読み込み(lazy loading。実際に使った瞬間に読み込むこと)をします。そのため、あとで使うと分かっているなら、先読み(eager loading)で、先にまとめて読み込むのがおすすめです。実行する SQL の数が大きく減ります。
リレーションがあるものを探す#
「コメントが1つ以上ある記事」のように、リレーションがあるかどうかで絞りたいときは、has と orHas を使います。
use App\Models\Post;
// コメントが1つ以上ある記事
$posts = Post::has('comments')->get();
演算子(>= のような比べる記号)と数も指定できます。
// コメントが3つ以上ある記事
$posts = Post::has('comments', '>=', 3)->get();
ドット(.)で、入れ子のリレーションも指定できます。
// 画像つきのコメントが1つ以上ある記事
$posts = Post::has('comments.images')->get();
もっと細かい条件には、whereHas と orWhereHas を使います。たとえば、コメントの中身を調べられます。
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 は、あるモデル(またはモデルのコレクション)と多対多で結びついているモデルを探します。
$users = User::whereAttachedTo($role)->get();
コレクションを渡すと、そのどれかに結びついているモデルが取れます。
$tags = Tag::whereLike('name', '%laravel%')->get();
$posts = Post::whereAttachedTo($tags)->get();
1行で書く検索#
リレーションに簡単な条件を1つ付けるだけなら、whereRelation・orWhereRelation・whereMorphRelation・orWhereMorphRelation が便利です。たとえば、承認されていないコメントがある記事です。
use App\Models\Post;
$posts = Post::whereRelation('comments', 'is_approved', false)->get();
where と同じく、演算子も指定できます。
$posts = Post::whereRelation(
'comments', 'created_at', '>=', now()->minus(hours: 1)
)->get();
リレーションがないものを探す#
「コメントが1つもない記事」のように、リレーションがないものを探すには、doesntHave と orDoesntHave を使います。
use App\Models\Post;
$posts = Post::doesntHave('comments')->get();
もっと細かい条件には、whereDoesntHave と orWhereDoesntHave を使います。
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', 1);
})->get();
Morph To のリレーションで探す#
morphTo のリレーションを探すには、whereHasMorph と whereDoesntHaveMorph を使います。第1引数はリレーションの名前、第2引数は対象のモデル、第3引数は条件を決めるクロージャです。
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 を受け取れます。
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引数に親のモデルを渡します。
$comments = Comment::whereMorphedTo('commentable', $post)
->orWhereMorphedTo('commentable', $video)
->get();
すべての種類を対象にする#
モデルの配列の代わりに * を渡すと、データベースにあるすべての種類を対象にします。そのために、Laravel は追加の問い合わせを1回実行します。
use Illuminate\Database\Eloquent\Builder;
$comments = Comment::whereHasMorph('commentable', '*', function (Builder $query) {
$query->where('title', 'like', 'foo%');
})->get();
関連するモデルを集計する#
数える(withCount)#
関連するモデルを読み込まずに、数だけ知りたいときは withCount を使います。結果のモデルに {リレーション名}_count という属性が付きます。
use App\Models\Post;
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count;
}
配列を渡すと、複数のリレーションを数えたり、条件を足したりできます。
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)#
親のモデルを取り出したあとで数えたいときは、loadCount を使います。
$book = Book::first();
$book->loadCount('genres');
条件を足したいときは、リレーション名をキーにした配列を渡し、値にクロージャを書きます。
$book->loadCount(['reviews' => function (Builder $query) {
$query->where('rating', 5);
}]);
select と組み合わせるとき#
withCount を select と組み合わせるなら、select のあとで withCount を呼びます。
$posts = Post::select(['title', 'body'])
->withCount('comments')
->get();
ほかの集計のメソッド#
withCount のほかに、次のメソッドがあります。結果には {リレーション名}_{関数}_{列名} という属性が付きます。
| メソッド | 説明 |
|---|---|
withMin |
関連するモデルの、列の最小値 |
withMax |
関連するモデルの、列の最大値 |
withAvg |
関連するモデルの、列の平均 |
withSum |
関連するモデルの、列の合計 |
withExists |
関連するモデルが存在するか(はい・いいえ) |
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 と同じく、あとから実行する版(loadSum など)もあります。
$post = Post::first();
$post->loadSum('comments', 'votes');
select と組み合わせるときは、これらも select のあとで呼びます。
$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 を持ちます。各親について、写真のタグの数と記事のコメントの数を取りたい場合です。
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 を使います。
$activities = ActivityFeed::with('parentable')->get();
$activities->loadMorphCount('parentable', [
Photo::class => ['tags'],
Post::class => ['comments'],
]);
先読み(Eager Loading)#
リレーションをプロパティとして使うと、必要になった瞬間に読み込む遅延読み込みになります。これに対し、親のモデルを取り出すときに、いっしょに読み込んでおくのが先読みです。先読みは「N + 1」問題を防ぎます。
Book(本)が Author(著者)に属する例で考えましょう。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Book extends Model
{
/**
* 本の著者を取り出す
*/
public function author(): BelongsTo
{
return $this->belongsTo(Author::class);
}
}
すべての本と著者を取り出します。
use App\Models\Book;
$books = Book::all();
foreach ($books as $book) {
echo $book->author->name;
}
このループは、本を取るのに1回、さらに各本の著者を取るのに1回ずつ、問い合わせを実行します。本が25冊なら、1 + 25 で、26回です。
先読みを使うと、2回で済みます。with で、先読みするリレーションを指定します。
$books = Book::with('author')->get();
foreach ($books as $book) {
echo $book->author->name;
}
実行されるのは、本を取る1回と、その著者をまとめて取る1回だけです。
select * from books
select * from authors where id in (1, 2, 3, 4, 5, ...)
複数のリレーションを先読みする#
配列で渡します。
$books = Book::with(['author', 'publisher'])->get();
入れ子のリレーションを先読みする#
ドット(.)で、リレーションのリレーションも先読みできます。
$books = Book::with('author.contacts')->get();
入れ子の配列でも書けます。
$books = Book::with([
'author' => [
'contacts',
'publisher',
],
])->get();
morphTo の入れ子を先読みする#
morphTo を先読みしつつ、返ってきた各モデルの入れ子のリレーションも先読みしたいときは、with と morphTo の morphWith を組み合わせます。次のモデルで考えます。
<?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 と、その入れ子のリレーションを先読みします。
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();
取り出す列を絞る#
リレーションの全部の列が要らないときは、列を指定できます。
$books = Book::with('author:id,name,book_id')->get();
注意
列を絞るときは、id の列と、関係する外部キーの列を、必ず含めてください。
いつも先読みする#
いつも読み込みたいリレーションは、モデルの $with プロパティに書きます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Book extends Model
{
/**
* いつも読み込むリレーション
*
* @var array
*/
protected $with = ['author'];
/**
* 本の著者を取り出す
*/
public function author(): BelongsTo
{
return $this->belongsTo(Author::class);
}
/**
* 本のジャンルを取り出す
*/
public function genre(): BelongsTo
{
return $this->belongsTo(Genre::class);
}
}
1回の検索だけ $with の一部を外したいときは、without を使います。
$books = Book::without('author')->get();
1回の検索だけ $with をまるごと置き換えたいときは、withOnly を使います。
$books = Book::withOnly('genre')->get();
先読みに条件を足す#
先読みの検索にも条件を足せます。リレーション名をキー、条件を足すクロージャを値にした配列を、with に渡します。
use App\Models\User;
$users = User::with(['posts' => function ($query) {
$query->where('title', 'like', '%code%');
}])->get();
この例では、題名に code を含む記事だけを先読みします。ほかのクエリビルダのメソッドも使えます。
$users = User::with(['posts' => function ($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();
この例では、隠されていない記事と、type が educational の動画だけを先読みします。
リレーションの存在と先読みを同時にする#
「条件に合う記事を持つユーザーだけを取り出し、その記事も先読みする」ような、存在の確認と先読みを同じ条件でしたいときは、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 ($condition) {
$books->load('author', 'publisher');
}
条件を足したいときは、リレーション名をキー、クロージャを値にした配列を渡します。
$author->load(['books' => function ($query) {
$query->orderBy('published_date', 'asc');
}]);
まだ読み込んでいないときだけ読み込むには、loadMissing を使います。
$book->loadMissing('author');
morphTo の入れ子をあとから先読みする#
morphTo と、返ってきた各モデルの入れ子のリレーションを、あとから先読みするには、loadMorph を使います。第1引数に morphTo のリレーション名、第2引数に、モデルとリレーションの組の配列を渡します。例には、先ほどの ActivityFeed を使います。
$activities = ActivityFeed::with('parentable')
->get()
->loadMorph('parentable', [
Event::class => ['calendar'],
Photo::class => ['tags'],
Post::class => ['author'],
]);
自動で先読みする(Automatic Eager Loading)#
多くの場合、Laravel は、使ったリレーションを自動で先読みできます。有効にするには、AppServiceProvider の boot メソッドで Model::automaticallyEagerLoadRelationships を呼びます。
use Illuminate\Database\Eloquent\Model;
/**
* アプリの起動時の処理
*/
public function boot(): void
{
Model::automaticallyEagerLoadRelationships();
}
有効にすると、まだ読み込んでいないリレーションを使った瞬間に、Laravel が自動で読み込みます。次の例を見てください。
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 で有効にできます。
$users = User::where('vip', true)->get();
return $users->withRelationshipAutoloading();
遅延読み込みを禁止する(Preventing Lazy Loading)#
先読みは、アプリを速くします。そこで、遅延読み込みを禁止することもできます。Model::preventLazyLoading を、AppServiceProvider の boot メソッドで呼びます。
引数に真偽値(true か false)を渡せます。たとえば、本番以外でだけ禁止すれば、うっかり遅延読み込みが残っていても、本番は止まらずに動きます。
use Illuminate\Database\Eloquent\Model;
/**
* アプリの起動時の処理
*/
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}
禁止したあとに遅延読み込みをすると、Eloquent は Illuminate\Database\LazyLoadingViolationException という例外(エラーを知らせるしくみ)を投げます。
handleLazyLoadingViolationUsing で、違反したときの動きを変えられます。たとえば、例外にせず、ログに記録するだけにできます。
Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {
$class = $model::class;
info("Attempted to lazy load [{$relation}] on model [{$class}].");
});
関連するモデルを保存・更新する#
save メソッド#
リレーションを通して、新しいモデルを足せます。たとえば、記事にコメントを足すとき、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 を動的プロパティではなく、メソッドで呼んでいる点に注意してください(リレーションそのものが必要だからです)。
複数をまとめて保存するときは saveMany です。
$post = Post::find(1);
$post->comments()->saveMany([
new Comment(['message' => 'A new comment.']),
new Comment(['message' => 'Another new comment.']),
]);
save と saveMany は、モデルを保存しますが、親にすでに読み込んであるリレーションには、新しいモデルを足しません。保存したあとにリレーションを使うなら、refresh で読み込み直します。
$post->comments()->save($comment);
$post->refresh();
// 新しく保存したコメントを含む、すべてのコメント
$post->comments;
まとめて保存する(push)#
モデルと、そのリレーションをすべて保存したいときは、push を使います。次の例では、記事と、そのコメント、コメントの著者が保存されます。
$post = Post::find(1);
$post->comments[0]->message = 'Message';
$post->comments[0]->author->name = 'Author Name';
$post->push();
pushQuietly は、イベント(「〜が起きた」という知らせ)を出さずに保存します。
$post->pushQuietly();
create メソッド#
create は、配列を受け取り、モデルを作って保存します。save は完成したモデルを受け取り、create は普通の配列を受け取る点がちがいます。作ったモデルが返ります。
use App\Models\Post;
$post = Post::find(1);
$comment = $post->comments()->create([
'message' => 'A new comment.',
]);
複数まとめて作るときは createMany です。
$post = Post::find(1);
$post->comments()->createMany([
['message' => 'A new comment.'],
['message' => 'Another new comment.'],
]);
createQuietly と createManyQuietly は、イベントを出さずに作ります。
$user = User::find(1);
$user->posts()->createQuietly([
'title' => 'Post title.',
]);
$user->posts()->createManyQuietly([
['title' => 'First post.'],
['title' => 'Second post.'],
]);
findOrNew・firstOrNew・firstOrCreate・updateOrCreate も、リレーションで使えます(Eloquent の基本を見てください)。
補足
create を使う前に、マスアサインメント(配列でまとめて値を入れること)の説明を読んでおいてください。Eloquent の基本にあります。
Belongs To の更新#
子のモデルを、別の親に付け替えるには、associate を使います。次の例は、User が Account に属する場合です。associate は、子の外部キーを設定します。
use App\Models\Account;
$account = Account::find(10);
$user->account()->associate($account);
$user->save();
親との結びつきを外すには、dissociate を使います。外部キーが null になります。
$user->account()->dissociate();
$user->save();
多対多の更新#
結びつける・外す(attach / detach)#
多対多では、attach で、中間の表に行を足して結びつけます。たとえば、ユーザーに役割を付けます。
use App\Models\User;
$user = User::find(1);
$user->roles()->attach($roleId);
中間の表に入れる追加の値を、配列で渡せます。
$user->roles()->attach($roleId, ['expires' => $expires]);
結びつきを外すには detach を使います。中間の表の行だけが消え、2つのモデルはデータベースに残ります。
// ユーザーから、1つの役割を外す
$user->roles()->detach($roleId);
// ユーザーから、すべての役割を外す
$user->roles()->detach();
attach と detach は、ID の配列も受け取れます。
$user = User::find(1);
$user->roles()->detach([1, 2, 3]);
$user->roles()->attach([
1 => ['expires' => $expires],
2 => ['expires' => $expires],
]);
そろえる(sync)#
sync は、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]);
入れ替える(toggle)#
toggle は、渡した ID の結びつきを「入れ替え」ます。結びついていれば外し、結びついていなければ結びつけます。
$user->roles()->toggle([1, 2, 3]);
中間の表の値も渡せます。
$user->roles()->toggle([
1 => ['expires' => true],
2 => ['expires' => true],
]);
トランザクションで動かす#
ここまでの中間の表の操作には、OrFail が付いた版があります(attachOrFail・detachOrFail・syncOrFail・syncWithoutDetachingOrFail・toggleOrFail)。トランザクション(まとめて成功するか、まとめて取り消すかにするしくみ)の中で実行され、例外が起きると、変更はすべて元に戻ります。
$user->roles()->attachOrFail([1, 2, 3]);
$user->roles()->syncOrFail([1, 2, 3]);
中間の表の行を更新する#
中間の表に、すでにある行を更新するには、updateExistingPivot を使います。相手のモデルの ID と、更新する値の配列を渡します。
$user = User::find(1);
$user->roles()->updateExistingPivot($roleId, [
'active' => false,
]);
親の更新日時を更新する(touch)#
belongsTo や belongsToMany で親に属しているとき、子が更新されたら、親の更新日時も更新したいことがあります。たとえば、Comment が更新されたら、親の Post の updated_at を現在の日時にしたい場合です。
子のモデルに Touches 属性を付けて、更新日時を更新したいリレーションの名前を並べます。
<?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日時点の内容をもとに、日本語でまとめています。