- はじめに
- リレーションシップの定義
- 多対多リレーションシップ
- ポリモーフィックリレーションシップ
- 動的リレーションシップ
- リレーションのクエリ
- 関連モデルの集計
- イーガーロード
- 関連モデルの挿入と更新
- 親のタイムスタンプを更新する
#はじめに
データベースのテーブルはしばしば互いに関連しています。例えば、ブログ投稿には多くのコメントが付くことがあり、注文はその注文を行ったユーザーに関連付けられます。Eloquent はこれらのリレーションシップの管理と操作を簡単にし、さまざまな一般的なリレーションシップをサポートしています。
#リレーションシップの定義
Eloquent のリレーションシップは、Eloquent モデルクラスのメソッドとして定義します。リレーションシップは強力なクエリビルダーとしても機能するため、メソッドとして定義することでメソッドチェーンやクエリの柔軟な操作が可能になります。例えば、posts リレーションシップに追加のクエリ制約をチェーンできます。
$user->posts()->where('active', 1)->get();
ただし、リレーションシップの使い方に深入りする前に、Eloquent がサポートする各リレーションシップの定義方法を学びましょう。
#1対1
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
{
/**
* ユーザーに関連付けられた電話を取得する。
*/
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 は外部キーの値が親の主キー列と一致すると想定します。つまり、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
{
/**
* 電話を所有するユーザーを取得する。
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
user メソッドを呼び出すと、Eloquent は Phone モデルの user_id 列と一致する id を持つ User モデルを探します。
Eloquent はリレーションシップメソッド名に _id を付けた名前を外部キー名として推測します。この場合、Phone モデルは user_id 列を持つと想定します。ただし、Phone モデルの外部キーが user_id でない場合は、belongsTo メソッドの第2引数にカスタムキー名を渡せます。
/**
* 電話を所有するユーザーを取得する。
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key');
}
親モデルが id を主キーとして使っていない場合や、別のカラムで関連モデルを検索したい場合は、belongsTo メソッドの第3引数に親テーブルのカスタムキーを指定できます。
/**
* 電話を所有するユーザーを取得する。
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
}
#1対多
1対多リレーションシップは、1つのモデルが複数の子モデルの親となる関係を定義します。例えば、ブログ投稿には無数のコメントが付くことがあります。すべての Eloquent リレーションシップと同様に、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);
}
}
Eloquent は Comment モデルの適切な外部キー列を自動的に決定します。規約として、親モデル名のスネークケースに _id を付けた名前を使います。この例では、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');
#1対多(逆) / belongsTo
投稿のすべてのコメントにアクセスできるようになったので、コメントから親の投稿にアクセスするリレーションシップを定義しましょう。hasMany の逆は子モデルに belongsTo メソッドを呼ぶリレーションシップメソッドを定義します。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Comment extends Model
{
/**
* コメントを所有する投稿を取得する。
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}
リレーションシップを定義したら、post の動的リレーションシッププロパティを通じてコメントの親投稿を取得できます。
use App\Models\Comment;
$comment = Comment::find(1);
return $comment->post->title;
上記の例では、Eloquent は Comment モデルの post_id 列と一致する id を持つ Post モデルを探します。
Eloquent はリレーションシップメソッド名に _ と親モデルの主キー名を付けた名前を外部キーとして推測します。この例では、comments テーブルの Post モデルの外部キーは post_id と想定されます。
ただし、外部キーがこの規約に従わない場合は、belongsTo メソッドの第2引数にカスタム外部キー名を渡せます。
/**
* コメントを所有する投稿を取得する。
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key');
}
親モデルが id を主キーとして使っていない場合や、別のカラムで関連モデルを検索したい場合は、belongsTo メソッドの第3引数に親テーブルのカスタムキーを指定できます。
/**
* コメントを所有する投稿を取得する。
*/
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 モデルを返します。
/**
* 投稿の作成者を取得します。
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault();
}
デフォルトモデルに属性を設定するには、withDefault メソッドに配列またはクロージャを渡せます。
/**
* 投稿の作成者を取得します。
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault([
'name' => 'Guest Author',
]);
}
/**
* 投稿の作成者を取得します。
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault(function (User $user, Post $post) {
$user->name = 'Guest Author';
});
}
#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();
#多数の中の1つを持つ
モデルが多くの関連モデルを持つ場合でも、リレーションの中で「最新」または「最古」の関連モデルだけを簡単に取得したいことがあります。例えば、User モデルが多くの Order モデルに関連しているが、ユーザーが最後に行った注文だけを扱いたい場合です。これは hasOne リレーションと ofMany メソッドを組み合わせて実現できます。
/**
* ユーザーの最新の注文を取得します。
*/
public function latestOrder(): HasOne
{
return $this->hasOne(Order::class)->latestOfMany();
}
同様に、リレーションの「最古」または最初の関連モデルを取得するメソッドも定義できます。
/**
* ユーザーの最古の注文を取得します。
*/
public function oldestOrder(): HasOne
{
return $this->hasOne(Order::class)->oldestOfMany();
}
デフォルトでは、latestOfMany と oldestOfMany メソッドはモデルの主キー(ソート可能なもの)に基づいて最新または最古の関連モデルを取得します。ただし、異なるソート基準で単一モデルを取得したい場合もあります。
例えば、ofMany メソッドを使うと、ユーザーの最も高額な注文を取得できます。ofMany は最初の引数にソート対象のカラムを、2番目の引数に集約関数(min または max)を指定します。
/**
* ユーザーの最大の注文を取得します。
*/
public function largestOrder(): HasOne
{
return $this->hasOne(Order::class)->ofMany('price', 'max');
}
PostgreSQL は UUID カラムに対して MAX 関数を実行できないため、PostgreSQL の UUID カラムと組み合わせて one-of-many リレーションを使うことは現在できません。
#"Many" リレーションを Has One リレーションに変換する
latestOfMany、oldestOfMany、ofMany メソッドで単一モデルを取得する際、同じモデルに対して既に "has many" リレーションが定義されていることが多いです。Laravel では利便性のため、そのリレーションに対して one メソッドを呼び出すことで簡単に "has one" リレーションに変換できます。
/**
* ユーザーの注文を取得します。
*/
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
/**
* ユーザーの最大の注文を取得します。
*/
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引数としてクロージャを渡します。このクロージャはリレーションのクエリに対して追加の公開日制約を付与する役割を担います:
/**
* 製品の現在の価格を取得します。
*/
public function currentPricing(): HasOne
{
return $this->hasOne(Price::class)->ofMany([
'published_at' => 'max',
'id' => 'max',
], function (Builder $query) {
$query->where('published_at', '<', now());
});
}
#中間を介した1対1リレーション
"has-one-through" リレーションは別のモデルとの一対一リレーションを定義しますが、このリレーションは宣言モデルが第三のモデルを経由して別のモデルのインスタンスと結びつくことを示します。
例えば、車の修理工場アプリケーションでは、各 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
{
/**
* 車のオーナーを取得します。
*/
public function carOwner(): HasOneThrough
{
return $this->hasOneThrough(Owner::class, Car::class);
}
}
hasOneThrough メソッドの第1引数は最終的にアクセスしたいモデル名、第2引数は中間モデル名です。
または、関係するすべてのモデルにリレーションが既に定義されている場合は、through メソッドを使ってそれらのリレーション名を指定し、流暢に "has-one-through" リレーションを定義できます。例えば、Mechanic モデルに cars リレーションがあり、Car モデルに owner リレーションがある場合、以下のように定義できます。
// 文字列ベースの構文...
return $this->through('cars')->has('owner');
// 動的構文...
return $this->throughCars()->hasOwner();
#キーの規約
リレーションのクエリ実行時には、一般的な Eloquent の外部キー規約が使われます。キーをカスタマイズしたい場合は、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 メソッドを使ってリレーション名を指定し、既存のリレーションで定義されたキー規約を再利用しながら "has-one-through" リレーションを流暢に定義できます。
// 文字列ベースの構文...
return $this->through('cars')->has('owner');
// 動的構文...
return $this->throughCars()->hasOwner();
#中間を介した1対多リレーション
"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
{
/**
* プロジェクトのすべてのデプロイを取得します。
*/
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(Deployment::class, Environment::class);
}
}
hasManyThrough メソッドの第1引数は最終的にアクセスしたいモデル名、第2引数は中間モデル名です。
または、関係するすべてのモデルにリレーションが既に定義されている場合は、through メソッドを使ってそれらのリレーション名を指定し、流暢に "has-many-through" リレーションを定義できます。例えば、Project モデルに environments リレーションがあり、Environment モデルに deployments リレーションがある場合、以下のように定義できます。
// 文字列ベースの構文...
return $this->through('environments')->has('deployments');
// 動的構文...
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', // environments テーブルの外部キー...
'environment_id', // deployments テーブルの外部キー...
'id', // projects テーブルのローカルキー...
'id' // environments テーブルのローカルキー...
);
}
}
前述のように、関係するすべてのモデルで関連リレーションがすでに定義されている場合は、through メソッドを呼び出してそれらのリレーション名を渡すことで、「has-many-through」リレーションを流暢に定義できます。この方法は既存のリレーションで定義されたキー規約を再利用できる利点があります。
// 文字列ベースの構文...
return $this->through('environments')->has('deployments');
// 動的構文...
return $this->throughEnvironments()->hasDeployments();
#多対多リレーション
多対多リレーションは、hasOne や hasMany リレーションよりもやや複雑です。多対多リレーションの例として、ユーザーが複数のロールを持ち、そのロールが他のユーザーとも共有されている場合があります。例えば、ユーザーは「Author」や「Editor」のロールを持つことがありますが、これらのロールは他のユーザーにも割り当てられることがあります。つまり、ユーザーは複数のロールを持ち、ロールも複数のユーザーに属しています。
#テーブル構造
このリレーションを定義するには、users、roles、role_user の3つのデータベーステーブルが必要です。role_user テーブルは関連モデル名のアルファベット順から派生し、user_id と role_id カラムを含みます。このテーブルはユーザーとロールをつなぐ中間テーブルとして使われます。
ロールは複数のユーザーに属する可能性があるため、単純に roles テーブルに user_id カラムを置くことはできません。そうするとロールは1人のユーザーにしか属せなくなります。複数のユーザーにロールを割り当てるために、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
{
/**
* ユーザーに属するロール。
*/
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 は関連モデル名をアルファベット順に結合します。ただし、この規約は自由に上書きできます。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
{
/**
* ロールに属するユーザー。
*/
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
}
ご覧の通り、リレーションは User モデル側とまったく同じ方法で定義されており、参照するモデルが App\Models\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_at と updated_at のタイムスタンプを持たせたい場合は、リレーション定義時に withTimestamps メソッドを呼び出します。
return $this->belongsToMany(Role::class)->withTimestamps();
Eloquent の自動管理タイムスタンプを使う中間テーブルには、created_at と updated_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 リレーションのクエリ結果を絞り込むには、リレーション定義時に wherePivot、wherePivotIn、wherePivotNotIn、wherePivotBetween、wherePivotNotBetween、wherePivotNull、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 クラスを継承します。例えば、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 モデルを定義する際は、Illuminate\Database\Eloquent\Relations\Pivot クラスを継承してください。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
class RoleUser extends Pivot
{
// ...
}
ピボットモデルは SoftDeletes トレイトを使えません。ピボットレコードをソフトデリートしたい場合は、ピボットモデルを通常の Eloquent モデルに変換することを検討してください。
#カスタムピボットモデルと自動増分ID
カスタムピボットモデルを使い、そのモデルに自動増分の主キーがある場合は、カスタムピボットモデルクラスに incrementing プロパティを true に設定してください。
/**
* IDが自動増分かどうかを示します。
*
* @var bool
*/
public $incrementing = true;
#多態リレーション
ポリモーフィックリレーションシップは、子モデルが単一の関連付けを使って複数のタイプのモデルに属することを可能にします。例えば、ユーザーがブログ投稿と動画を共有できるアプリケーションを構築するとします。このようなアプリケーションでは、CommentモデルがPostモデルとVideoモデルの両方に属する場合があります。
#1対1(ポリモーフィック)
#テーブル構造
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_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
{
/**
* 親のimageableモデル(ユーザーまたは投稿)を取得します。
*/
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を呼び出すメソッド名にアクセスします。この場合はImageモデルのimageableメソッドです。したがって、そのメソッドを動的リレーションプロパティとしてアクセスします。
use App\Models\Image;
$image = Image::find(1);
$imageable = $image->imageable;
Imageモデルのimageableリレーションは、画像を所有するモデルのタイプに応じてPostまたはUserのインスタンスを返します。
#キーの規約
必要に応じて、ポリモーフィック子モデルで使用する「id」と「type」カラムの名前を指定できます。その場合、morphToメソッドの最初の引数に必ずリレーション名を渡してください。通常、この値はメソッド名と一致するため、PHPの__FUNCTION__定数を使うことができます。
/**
* 画像が属するモデルを取得します。
*/
public function imageable(): MorphTo
{
return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');
}
#1対多(ポリモーフィック)
#テーブル構造
1対多のポリモーフィックリレーションは、通常の1対多リレーションに似ていますが、子モデルが単一の関連付けを使って複数のタイプのモデルに属することができます。例えば、アプリケーションのユーザーが投稿や動画に「コメント」できるとします。ポリモーフィックリレーションを使うことで、commentsテーブル1つで投稿と動画の両方のコメントを管理できます。まずはこのリレーションを構築するためのテーブル構造を見てみましょう。
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
{
/**
* 親のcommentableモデル(投稿または動画)を取得します。
*/
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を呼び出すメソッド名にアクセスします。この場合はCommentモデルのcommentableメソッドです。したがって、そのメソッドを動的リレーションプロパティとしてアクセスし、コメントの親モデルにアクセスします。
use App\Models\Comment;
$comment = Comment::find(1);
$commentable = $comment->commentable;
Commentモデルのcommentableリレーションは、コメントの親モデルのタイプに応じてPostまたはVideoのインスタンスを返します。
#1つの中の1つ(ポリモーフィック)
モデルが多くの関連モデルを持つ場合でも、リレーションの「最新」または「最古」の関連モデルを簡単に取得したいことがあります。例えば、Userモデルが多くのImageモデルに関連しているが、ユーザーがアップロードした最新の画像に便利にアクセスしたい場合です。これはmorphOneリレーションタイプとofManyメソッドを組み合わせて実現できます。
/**
* ユーザーの最新の画像を取得します。
*/
public function latestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->latestOfMany();
}
同様に、リレーションの「最古」または最初の関連モデルを取得するメソッドも定義できます。
/**
* ユーザーの最古の画像を取得します。
*/
public function oldestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->oldestOfMany();
}
デフォルトでは、latestOfManyとoldestOfManyメソッドはモデルの主キー(ソート可能なもの)に基づいて最新または最古の関連モデルを取得します。しかし、異なるソート基準で大きなリレーションから単一のモデルを取得したい場合もあります。
例えば、ofManyメソッドを使うと、ユーザーの最も「いいね」が多い画像を取得できます。ofManyメソッドは最初の引数にソート対象のカラム名を取り、関連モデルをクエリする際に適用する集約関数(minまたはmax)を指定します。
/**
* ユーザーの最も人気のある画像を取得します。
*/
public function bestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');
}
より高度な「1つの中の1つ」リレーションを構築することも可能です。詳細は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
ポリモーフィック多対多リレーションに入る前に、通常の多対多リレーションのドキュメントを読むと理解が深まります。
#モデル構造
次に、モデル上でリレーションを定義します。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
{
/**
* 投稿に関連付けられたすべてのタグを取得します。
*/
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) {
// ...
}
ポリモーフィック子モデルからポリモーフィック親を取得するには、morphedByMany を呼び出すメソッド名にアクセスします。この場合、Tag モデルの posts または videos メソッドが該当します。
use App\Models\Tag;
$tag = Tag::find(1);
foreach ($tag->posts as $post) {
// ...
}
foreach ($tag->videos as $video) {
// ...
}
#カスタムポリモーフィックタイプ
デフォルトでは、Laravel は関連モデルの「タイプ」を保存する際に完全修飾クラス名を使用します。例えば、上記の one-to-many の例で Comment モデルが Post または Video モデルに属する場合、デフォルトの commentable_type はそれぞれ App\Models\Post または App\Models\Video になります。ただし、これらの値をアプリケーションの内部構造から切り離したい場合があります。
例えば、モデル名の代わりに post や video のような単純な文字列を「タイプ」として使うことができます。こうすることで、モデル名が変更されてもデータベースのポリモーフィック「タイプ」カラムの値は有効なまま維持されます。
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 カラムの値が完全修飾クラス名のまま残っている場合、それらをマップ名に変換する必要があります。
#動的リレーションシップ
resolveRelationUsing メソッドを使うと、実行時に Eloquent モデル間のリレーションを定義できます。通常のアプリケーション開発ではあまり推奨されませんが、Laravel パッケージ開発時に役立つことがあります。
resolveRelationUsing メソッドは、最初の引数にリレーション名を受け取ります。第二引数にはモデルインスタンスを受け取り、有効な Eloquent リレーション定義を返すクロージャを渡します。通常は サービスプロバイダー の boot メソッド内で動的リレーションを設定します。
use App\Models\Order;
use App\Models\Customer;
Order::resolveRelationUsing('customer', function (Order $orderModel) {
return $orderModel->belongsTo(Customer::class, 'customer_id');
});
動的リレーションを定義する際は、必ず 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
{
/**
* ユーザーのすべての投稿を取得します。
*/
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 リレーションのクエリに追加制約が不要な場合は、リレーションをプロパティのようにアクセスできます。例えば、User と Post の例で、ユーザーのすべての投稿に以下のようにアクセスできます。
use App\Models\User;
$user = User::find(1);
foreach ($user->posts as $post) {
// ...
}
動的リレーションプロパティは「遅延ロード」を行い、実際にアクセスしたときにのみリレーションデータをロードします。そのため、開発者はモデルロード後にアクセスするリレーションを事前にロードするために イーガーロード をよく使います。イーガーロードはモデルのリレーションをロードするための SQL クエリ数を大幅に削減します。
#リレーションの存在をクエリする
モデルレコードを取得する際、リレーションの存在に基づいて結果を絞り込みたい場合があります。例えば、少なくとも1つのコメントがあるブログ投稿を取得したい場合、has と orHas メソッドにリレーション名を渡します。
use App\Models\Post;
// コメントが1つ以上ある投稿をすべて取得...
$posts = Post::has('comments')->get();
演算子やカウント値を指定してクエリをさらにカスタマイズできます。
// コメントが3つ以上ある投稿をすべて取得...
$posts = Post::has('comments', '>=', 3)->get();
ネストした has 文は「ドット」表記で構築できます。例えば、少なくとも1つのコメントがあり、そのコメントに少なくとも1つの画像がある投稿を取得できます。
// コメントに画像が1つ以上ある投稿を取得...
$posts = Post::has('comments.images')->get();
さらに強力なクエリを作成したい場合は、whereHas と orWhereHas メソッドを使って、コメントの内容を調べるなどの追加制約を has クエリに定義できます。
use Illuminate\Database\Eloquent\Builder;
// content に code% のような単語を含むコメントが1つ以上ある投稿を取得...
$posts = Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
})->get();
// content に code% のような単語を含むコメントが10個以上ある投稿を取得...
$posts = Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
}, '>=', 10)->get();
Eloquent は現在、データベースをまたいだリレーションの存在クエリをサポートしていません。リレーションは同じデータベース内に存在する必要があります。
#インラインのリレーション存在クエリ
リレーションの存在を単一のシンプルな where 条件でクエリしたい場合、whereRelation、orWhereRelation、whereMorphRelation、orWhereMorphRelation メソッドを使うと便利です。例えば、承認されていないコメントがある投稿をクエリできます。
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 リレーションのクエリ
「morph to」リレーションの存在をクエリするには、whereHasMorph と whereDoesntHaveMorph メソッドを使います。これらのメソッドは最初の引数にリレーション名を受け取り、次にクエリに含めたい関連モデルの名前を受け取ります。最後にリレーションクエリをカスタマイズするクロージャを渡せます。
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();
関連するポリモーフィックモデルの「タイプ」に基づいてクエリ制約を追加する必要がある場合があります。whereHasMorph メソッドに渡すクロージャは第二引数に $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();
#すべての関連モデルをクエリする
可能なポリモーフィックモデルの配列を渡す代わりに、ワイルドカード値として * を指定できます。これにより、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);
}])
#リレーションカウントとカスタムセレクト文
withCount と select 文を組み合わせる場合は、select の後に withCount を呼び出すようにしてください:
$posts = Post::select(['title', 'body'])
->withCount('comments')
->get();
#その他の集計関数
withCount に加えて、Eloquent は withMin、withMax、withAvg、withSum、withExists メソッドも提供します。これらのメソッドは結果のモデルに {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();
#Morph To リレーションの関連モデルカウント
"morph to" リレーションと、そのリレーションが返す可能性のある各エンティティの関連モデルカウントをイーガーロードしたい場合は、with メソッドと morphTo リレーションの morphWithCount メソッドを組み合わせて使えます。
この例では、Photo と Post モデルが ActivityFeed モデルを作成すると仮定します。ActivityFeed モデルは parentable という "morph to" リレーションを定義し、特定の ActivityFeed インスタンスの親である Photo または Post モデルを取得できます。さらに、Photo モデルは多くの Tag モデルを持ち、Post モデルは多くの Comment モデルを持つと仮定します。
ここでは、ActivityFeed のインスタンスを取得し、各 ActivityFeed インスタンスに対して parentable の親モデルを Eagerロードしたいとします。さらに、各親の写真に関連付けられたタグの数と、各親の投稿に関連付けられたコメントの数も取得したい場合を考えます:
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'],
]);
#イーガーロード
Eloquent のリレーションシップにプロパティとしてアクセスすると、関連モデルは「遅延ロード」されます。つまり、プロパティに初めてアクセスするまでリレーションデータは実際にはロードされません。しかし、Eloquent は親モデルをクエリするときにリレーションを「イーガーロード」できます。イーガーロードは「N + 1」クエリ問題を軽減します。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回のクエリを実行し、その後各本の著者を取得するために本の数だけ追加のクエリを実行します。たとえば25冊の本がある場合、合計で26回のクエリが実行されます:1回は本の取得、25回は各本の著者取得です。
幸いなことに、イーガーロードを使うとこの処理を2回のクエリに減らせます。クエリを構築するときに、with メソッドでイーガーロードするリレーションを指定できます:
$books = Book::with('author')->get();
foreach ($books as $book) {
echo $book->author->name;
}
この操作では、すべての本を取得するクエリと、すべての本の著者を取得するクエリの2回だけ実行されます:
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 リレーションと、そのリレーションが返す可能性のある各エンティティのネストしたリレーションをイーガーロードしたい場合は、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 モデルに属すると仮定します。
これらのモデル定義とリレーションを使い、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();
#特定カラムのイーガーロード
リレーションからすべてのカラムが必要とは限りません。そのため、取得したいリレーションのカラムを指定できます:
$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);
}
}
単一のクエリで $with プロパティから特定のリレーションを除外したい場合は、without メソッドを使えます:
$books = Book::without('author')->get();
単一のクエリで $with プロパティのすべてのリレーションを上書きしたい場合は、withOnly メソッドを使えます:
$books = Book::withOnly('genre')->get();
#イーガーロードの制約
リレーションをイーガーロードするときに、イーガーロードクエリに追加の条件を指定したい場合があります。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();
イーガーロードの制約時には、limit と take のクエリビルダーメソッドは使用できません。
#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 リレーション名、第二引数にモデルとリレーションのペアの配列を受け取ります。例として以下のモデルを考えます。
<?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 モデルに属するとします。
これらのモデル定義とリレーションを使い、ActivityFeed モデルを取得し、すべての parentable モデルとそれぞれのネストされたリレーションをイーガーロードできます。
$activities = ActivityFeed::with('parentable')
->get()
->loadMorph('parentable', [
Event::class => ['calendar'],
Photo::class => ['tags'],
Post::class => ['author'],
]);
#遅延ロードの防止
前述のように、リレーションのイーガーロードはアプリケーションのパフォーマンス向上に大きく寄与します。したがって、必要に応じて Laravel にリレーションの遅延ロードを常に防止させることができます。これを行うには、ベースの Eloquent モデルクラスが提供する preventLazyLoading メソッドを呼び出します。通常はアプリケーションの AppServiceProvider クラスの boot メソッド内で呼び出します。
preventLazyLoading メソッドは、遅延ロードを防止するかどうかを示すオプションのブール引数を受け取ります。例えば、本番環境以外でのみ遅延ロードを無効にし、本番環境では誤って遅延ロードがあっても正常に動作させたい場合に使えます。
use Illuminate\Database\Eloquent\Model;
/**
* アプリケーションサービスを初期化します。
*/
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}
遅延ロードを防止すると、アプリケーションが 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();
// 新しく保存したコメントを含むすべてのコメント...
$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 メソッドも使用できます。save と create の違いは、save が完全な Eloquent モデルインスタンスを受け取るのに対し、create はプレーンな PHP の array を受け取る点です。新しく作成されたモデルは 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 メソッドもリレーション上でのモデルの作成・更新に使えます。
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();
#Many to Many リレーション
#アタッチ / デタッチ
Eloquent は多対多リレーションの操作を便利にするメソッドも提供します。例えば、ユーザーが複数のロールを持ち、ロールも複数のユーザーを持つ場合、attach メソッドで中間テーブルにレコードを挿入してユーザーにロールを割り当てられます。
use App\Models\User;
$user = User::find(1);
$user->roles()->attach($roleId);
リレーションをアタッチする際に、中間テーブルに挿入する追加データの配列を渡すこともできます。
$user->roles()->attach($roleId, ['expires' => $expires]);
ユーザーからロールを外す必要がある場合は、detach メソッドを使います。detach は中間テーブルの該当レコードを削除しますが、両モデルはデータベースに残ります。
// ユーザーから単一のロールを外す...
$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],
]);
#同期(Syncing)操作
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を切り離さずに、指定した配列にない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 リレーションシップを定義している場合、例えば Comment が Post に属しているようなケースでは、子モデルが更新された際に親モデルのタイムスタンプを更新すると便利です。
例えば、Comment モデルが更新されたときに、所有する Post の updated_at タイムスタンプを自動的に「タッチ」して現在日時に設定したい場合があります。これを実現するには、子モデルに touches プロパティを追加し、子モデルが更新されたときに updated_at タイムスタンプを更新したいリレーション名を指定します。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Comment extends Model
{
/**
* タッチするリレーションシップの一覧。
*
* @var array
*/
protected $touches = ['post'];
/**
* コメントが属する投稿を取得する。
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}
親モデルのタイムスタンプは、子モデルが Eloquent の save メソッドで更新された場合にのみ更新されます。