エロクエント: リレーションシップ
はじめに
データベースのテーブルはしばしば関連しています。たとえば、ブログ投稿には多くのコメントが付いているか、注文はそれを行ったユーザーと関連しているかもしれません。Eloquentを使用すると、これらの関係を管理および操作することが簡単になり、さまざまな一般的な関係をサポートしています。
関係の定義
Eloquentの関係は、Eloquentモデルクラスのメソッドとして定義されます。関係は強力なクエリビルダとしても機能するため、メソッドとして関係を定義することで、強力なメソッドチェーンおよびクエリ機能が提供されます。たとえば、この posts
関係に追加のクエリ制約をチェーンすることができます:
$user->posts()->where('active', 1)->get();
しかし、関係を使用する際に深く掘り下げる前に、Eloquentでサポートされている各種類の関係を定義する方法を学びましょう。
一対一
一対一の関係は非常に基本的なデータベースの関係の一種です。たとえば、User
モデルは1つの Phone
モデルに関連付けられるかもしれません。この関係を定義するには、User
モデルに phone
メソッドを配置します。phone
メソッドは hasOne
メソッドを呼び出し、その結果を返す必要があります。hasOne
メソッドは、モデルの Illuminate\Database\Eloquent\Model
ベースクラスを介してモデルに利用可能です:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
class User extends Model
{
/**
* Get the phone associated with the user.
*/
public function phone(): HasOne
{
return $this->hasOne(Phone::class);
}
}
hasOne
メソッドに渡される最初の引数は関連するモデルクラスの名前です。関係が定義されると、Eloquentのダイナミックプロパティを使用して関連レコードを取得できます。ダイナミックプロパティを使用すると、モデルで定義されたプロパティのように関係メソッドにアクセスできます。
$phone = User::find(1)->phone;
Eloquentは、親モデル名に基づいて関係の外部キーを決定します。この場合、Phone
モデルは自動的にuser_id
外部キーを持つものと見なされます。この規則をオーバーライドしたい場合は、hasOne
メソッドに第2引数を渡すことができます:
return $this->hasOne(Phone::class, 'foreign_key');
さらに、Eloquentは外部キーが親の主キー列と一致する値を持つものと仮定します。つまり、EloquentはPhone
レコードのuser_id
列にユーザーのid
列の値を探します。関係をid
以外の主キー値やモデルの$primaryKey
プロパティを使用したい場合は、hasOne
メソッドに第3引数を渡すことができます:
return $this->hasOne(Phone::class, 'foreign_key', 'local_key');
関係の逆を定義する
したがって、User
モデルからPhone
モデルにアクセスできます。次に、Phone
モデルで、電話を所有するユーザーにアクセスできる関係を定義しましょう。belongsTo
メソッドを使用して、hasOne
関係の逆を定義できます:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Phone extends Model
{
/**
* Get the user that owns the phone.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
user
メソッドを呼び出すと、EloquentはPhone
モデルのuser_id
列と一致するid
を持つUser
モデルを見つけようとします。
Eloquentは、関係メソッドの名前を調べ、メソッド名に_id
を付け加えたものを外部キー名として決定します。したがって、この場合、EloquentはPhone
モデルがuser_id
列を持つものと仮定します。ただし、Phone
モデルの外部キーがuser_id
でない場合は、belongsTo
メソッドの第2引数としてカスタムキー名を渡すことができます:
/**
* Get the user that owns the phone.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key');
}
親モデルが主キーとしてid
を使用していない場合や、異なる列を使用して関連するモデルを見つけたい場合は、belongsTo
メソッドに第3引数を渡して親テーブルのカスタムキーを指定できます:
/**
* Get the user that owns the phone.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
}
一対多
一対多の関係は、1つのモデルが1つ以上の子モデルに親である関係を定義するために使用されます。例えば、ブログ投稿には無限のコメントが付くことがあります。他のすべてのEloquentの関係と同様に、一対多の関係は、Eloquentモデルでメソッドを定義することで定義されます:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Post extends Model
{
/**
* Get the comments for the blog post.
*/
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}
EloquentはComment
モデルの適切な外部キーカラムを自動的に決定します。慣例として、Eloquentは親モデルの「スネークケース」の名前を取り、_id
をサフィックスとして付け加えます。したがって、この例では、EloquentはComment
モデルの外部キーカラムをpost_id
と仮定します。
関係メソッドが定義されたら、comments
プロパティにアクセスすることで関連するコメントのコレクションにアクセスできます。Eloquentは「動的関係プロパティ」を提供しているため、関係メソッドにアクセスする際には、モデルのプロパティとして定義されているかのようにアクセスできます:
use App\Models\Post;
$comments = Post::find(1)->comments;
foreach ($comments as $comment) {
// ...
}
すべての関係はクエリビルダーとしても機能するため、comments
メソッドを呼び出してクエリにさらなる制約を追加し、クエリに条件を追加することができます:
$comment = Post::find(1)->comments()
->where('title', 'foo')
->first();
hasOne
メソッドと同様に、hasMany
メソッドに追加の引数を渡すことで、外部キーとローカルキーを上書きすることもできます:
return $this->hasMany(Comment::class, 'foreign_key');
return $this->hasMany(Comment::class, 'foreign_key', 'local_key');
一対多(逆)/ Belongs To
今やすべての投稿のコメントにアクセスできるようになったので、コメントが親投稿にアクセスできるようにする関係を定義しましょう。hasMany
関係の逆を定義するには、子モデルでbelongsTo
メソッドを呼び出す関係メソッドを定義します:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Comment extends Model
{
/**
* Get the post that owns the comment.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}
関係が定義されたら、post
「動的関係プロパティ」にアクセスすることで、コメントの親投稿を取得できます:
use App\Models\Comment;
$comment = Comment::find(1);
return $comment->post->title;
上記の例では、Eloquentは、Comment
モデルのpost_id
カラムに一致するid
を持つPost
モデルを見つけようとします。
Eloquentは、関係メソッドの名前を調べ、メソッド名に _
を付けて親モデルの主キーカラムの名前を続けることで、デフォルトの外部キー名を決定します。したがって、この例では、Eloquentはcomments
テーブル上のPost
モデルの外部キーをpost_id
と仮定します。
ただし、関係の外部キーがこれらの規則に従わない場合は、belongsTo
メソッドの第2引数としてカスタムの外部キー名を渡すことができます:
/**
* Get the post that owns the comment.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key');
}
親モデルが主キーとしてid
を使用していない場合、または異なるカラムを使用して関連するモデルを見つけたい場合は、belongsTo
メソッドに第3引数を渡して親テーブルのカスタムキーを指定できます:
/**
* Get the post that owns the comment.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');
}
デフォルトモデル
belongsTo
、hasOne
、hasOneThrough
、およびmorphOne
の関係を使用すると、指定された関係がnull
の場合に返されるデフォルトモデルを定義できます。このパターンはしばしばNull Objectパターンと呼ばれ、コード内の条件付きチェックを削除するのに役立ちます。次の例では、user
関係は、Post
モデルにユーザーが関連付けられていない場合に、空のApp\Models\User
モデルを返します:
/**
* Get the author of the post.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault();
}
デフォルトモデルに属性を設定するには、withDefault
メソッドに配列またはクロージャを渡すことができます:
/**
* Get the author of the post.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault([
'name' => 'Guest Author',
]);
}
/**
* Get the author of the post.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault(function (User $user, Post $post) {
$user->name = 'Guest Author';
});
}
Belongs To関係のクエリ
"belongs to"関係の子をクエリする場合、対応するEloquentモデルを取得するためにwhere
句を手動で構築することができます:
use App\Models\Post;
$posts = Post::where('user_id', $user->id)->get();
ただし、指定されたモデルに適切な関係と外部キーを自動的に決定するwhereBelongsTo
メソッドを使用する方が便利かもしれません:
$posts = Post::whereBelongsTo($user)->get();
また、whereBelongsTo
メソッドにコレクションイン スタンスを提供することもできます。これを行うと、Laravelはコレクション内の親モデルのいずれかに属するモデルを取得します。
$users = User::where('vip', true)->get();
$posts = Post::whereBelongsTo($users)->get();
デフォルトでは、Laravel は指定されたモデルのクラス名に基づいて関連付けられた関係を決定します。ただし、whereBelongsTo
メソッドの第2引数として関係名を手動で指定することで、関係名を明示的に指定することもできます。
$posts = Post::whereBelongsTo($user, 'author')->get();
1つの中の1つ
時には、モデルには多くの関連モデルがあるかもしれませんが、関係の中で「最新」または「最古」の関連モデルを簡単に取得したい場合があります。たとえば、User
モデルは多くの Order
モデルに関連しているかもしれませんが、ユーザーが最後に注文した注文と便利にやり取りする方法を定義したい場合があります。これは、hasOne
関係タイプと ofMany
メソッドを組み合わせて達成できます。
/**
* Get the user's most recent order.
*/
public function latestOrder(): HasOne
{
return $this->hasOne(Order::class)->latestOfMany();
}
同様に、関係の中で「最古」または最初の関連モデルを取得するメソッドを定義することもできます。
/**
* Get the user's oldest order.
*/
public function oldestOrder(): HasOne
{
return $this->hasOne(Order::class)->oldestOfMany();
}
デフォルトでは、latestOfMany
メソッドと oldestOfMany
メソッドは、モデルの主 キーに基づいて最新または最古の関連モデルを取得します。ただし、大きな関係から異なる並べ替え基準を使用して単一のモデルを取得したい場合があります。
たとえば、ofMany
メソッドを使用して、ユーザーの最も高価な注文を取得することができます。ofMany
メソッドは、最初の引数として並べ替え可能な列を受け入れ、関連モデルをクエリする際に適用する集計関数(min
または max
)を指定します。
/**
* Get the user's largest order.
*/
public function largestOrder(): HasOne
{
return $this->hasOne(Order::class)->ofMany('price', 'max');
}
PostgreSQL は UUID 列に対して MAX
関数を実行することをサポートしていないため、現在のところ PostgreSQL UUID 列との組み合わせで 1 つの中の多数の関係を使用することはできません。
"多数" の関係を 1 つの関係に変換する
しばしば、latestOfMany
、oldestOfMany
、または ofMany
メソッドを使用して単一のモデルを取得する際に、同じモデルに対して "has many" 関係が既に定義されている場合があります。便宜上、Laravel では、この関係を簡単に "has one" 関係に変換することができるように、関係に one
メソッドを呼び出すことができます。
/**
* Get the user's orders.
*/
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
/**
* Get the user's largest order.
*/
public function largestOrder(): HasOne
{
return $this->orders()->one()->ofMany('price', 'max');
}
より高度な1対多の関係
より高度な「1対多」の関係を構築することが可能です。たとえば、Product
モデルは、新しい価格が公開された後もシステムに保持される多くの関連するPrice
モデルを持つことがあります。さらに、製品の新しい価格データは、将来の日付に効果を発揮するために事前に公開できるように、published_at
列を介して公開される可能性があります。
したがって、要約すると、将来の日付ではない公開日が最新の公開価格を取得する必要があります。さらに、2つの価格が同じ公開日を持つ場合、IDが最も大きい価格を優先します。これを達成するには、最新の価格を決定するソート可能な列を含む配列をofMany
メソッドに渡す必要があります。さらに、2番目の引数としてofMany
メソッドにクロージャを提供します。このクロージャは、関係クエリに追加の公開日制約を追加する責任があります:
/**
* Get the current pricing for the product.
*/
public function currentPricing(): HasOne
{
return $this->hasOne(Price::class)->ofMany([
'published_at' => 'max',
'id' => 'max',
], function (Builder $query) {
$query->where('published_at', '<', now());
});
}
1対1を通じた関係
「1対1を通じた」関係は、別のモデルとの1対1の関係を定義します。ただし、この関係は、宣言モデルが第三のモデルを介して進むことで、別のモデルの1つのインスタンスと一致することを示します。
たとえば、車両修理店のアプリケーションでは、各Mechanic
モデルは1つのCar
モデルに関連付けられる場合があり、各Car
モデルは1つのOwner
モデルに関連付けられる場合があります。メカニックとオーナーはデータベース内で直接的な関係を持っていませんが、メカニックはCar
モデルを介してオーナーにアクセスできます。この関係を定義するために必要なテーブルを見てみましょう:
mechanics
id - integer
name - string
cars
id - integer
model - string
mechanic_id - integer
owners
id - integer
name - string
car_id - integer
この関係のテーブル構造を調べたので、Mechanic
モデルでこの関係を定義しましょう:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
class Mechanic extends Model
{
/**
* Get the car's owner.
*/
public function carOwner(): HasOneThrough
{
return $this->hasOneThrough(Owner::class, Car::class);
}
}
hasOneThrough
メソッドに渡され る最初の引数は、アクセスしたい最終モデルの名前であり、2番目の引数は中間モデルの名前です。
または、関連する関係がすでに関係するすべてのモデルで定義されている場合、through
メソッドを呼び出してこれらの関係の名前を指定することで、「has-one-through」関係をスムーズに定義することができます。たとえば、Mechanic
モデルに cars
関係があり、Car
モデルに owner
関係がある場合、次のようにメカニックと所有者を接続する「has-one-through」関係を定義できます:
// String based syntax...
return $this->through('cars')->has('owner');
// Dynamic syntax...
return $this->throughCars()->hasOwner();
キーの規則
関係のクエリを実行する際には、通常の Eloquent 外部キーの規則が使用されます。関係のキーをカスタマイズしたい場合は、hasOneThrough
メソッドの第三引数と第四引数としてそれらを渡すことができます。第三引数は中間モデルの外部キーの名前です。第四引数は最終モデルの外部キーの名前です。第五引数はローカルキーであり、第六引数は中間モデルのローカルキーです:
class Mechanic extends Model
{
/**
* Get the car's owner.
*/
public function carOwner(): HasOneThrough
{
return $this->hasOneThrough(
Owner::class,
Car::class,
'mechanic_id', // Foreign key on the cars table...
'car_id', // Foreign key on the owners table...
'id', // Local key on the mechanics table...
'id' // Local key on the cars table...
);
}
}
または、先に述べたように、関連する関係がすでに関係するすべてのモデルで定義されている場合、through
メソッドを呼び出してこれらの関係の名前を指定することで、「has-one-through」関係をスムーズに定義することができます。このアプローチは、既存の関係で定義されたキーの規則を再利用する利点を提供します:
// String based syntax...
return $this->through('cars')->has('owner');
// Dynamic syntax...
return $this->throughCars()->hasOwner();
Has Many Through
「has-many-through」関係は、中間関係を介して遠い関係にアクセスする便利な方法を提供します。たとえば、Laravel Vapor のようなデプロイメントプラットフォームを構築しているとします。Project
モデルは、中間の Environment
モデルを介して多くの Deployment
モデルにアクセスする可能性があります。この例を使用すると、特定のプロジェクトのすべてのデプロイメントを簡単に収集できま す。この関係を定義するために必要なテーブルを見てみましょう:
projects
id - integer
name - string
environments
id - integer
project_id - integer
name - string
deployments
id - integer
environment_id - integer
commit_hash - string
今回は、関係のテーブル構造を調査しましたので、Project
モデルで関係を定義しましょう:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
class Project extends Model
{
/**
* Get all of the deployments for the project.
*/
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(Deployment::class, Environment::class);
}
}
hasManyThrough
メソッドに渡される最初の引数は、アクセスしたい最終モデルの名前であり、2番目の引数は中間モデルの名前です。
また、関係するモデルすべてで関係がすでに定義されている場合は、through
メソッドを呼び出してこれらの関係の名前を指定することで、「has-many-through」関係を流暢に定義することもできます。例えば、Project
モデルに environments
関係があり、Environment
モデルに deployments
関係がある場合、プロジェクトとデプロイメントを接続する「has-many-through」関係を次のように定義できます:
// String based syntax...
return $this->through('environments')->has('deployments');
// Dynamic syntax...
return $this->throughEnvironments()->hasDeployments();
Deployment
モデルのテーブルに project_id
カラムが含まれていない場合でも、hasManyThrough
関係を使用すると、$project->deployments
を介してプロジェクトのデプロイメントにアクセスできます。これらのモデルを取得するために、Eloquent は中間の Environment
モデルのテーブルの project_id
カラムを調査します。関連する環境 ID を見つけた後、それらは Deployment
モデルのテーブルをクエリするために使用されます。
キーの規則
関係のクエリを実行する際には、通常の Eloquent 外部キーの規則が使用されます。関係のキーをカスタマイズしたい場合は、hasManyThrough
メソッドの3番目と4番目の引数としてそれらを渡すことがで きます。3番目の引数は中間モデルの外部キーの名前であり、4番目の引数は最終モデルの外部キーの名前です。5番目の引数はローカルキーであり、6番目の引数は中間モデルのローカルキーです:
class Project extends Model
{
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(
Deployment::class,
Environment::class,
'project_id', // Foreign key on the environments table...
'environment_id', // Foreign key on the deployments table...
'id', // Local key on the projects table...
'id' // Local key on the environments table...
);
}
}
また、先に述べたように、関係するモデルすべてで関係がすでに定義されている場合は、through
メソッドを呼び出してこれらの関係の名前を指定することで、「has-many-through」関係を流暢に定義することもできます。このアプローチは、既存の関係で定義されているキーの規則を再利用する利点があります:```
// String based syntax...
return $this->through('environments')->has('deployments');
// Dynamic syntax...
return $this->throughEnvironments()->hasDeployments();
多対多の関係
多対多の関係は、hasOne
や hasMany
の関係よりもやや複雑です。多対多の関係の例としては、ユーザーが多くの役割を持ち、その役割がアプリケーション内の他のユーザーとも共有される関係があります。たとえば、ユーザーが「著者」と「編集者」の役割を割り当てられる場合、これらの役割は他のユーザーにも割り当てられる可能性があります。つまり、ユーザーは多くの役割を持ち、役割は多くのユーザーを持つことができます。
テーブル構造
この関係を定義するためには、users
、roles
、role_user
の 3 つのデータベーステーブルが必要です。role_user
テーブルは、関連するモデル名のアルファベット順に派生し、user_id
と role_id
の列を含んでいます。このテーブルは、ユーザーと役割をリンクする中間テーブルとして使用されます。
役割が多くのユーザーに属する可能性があるため、roles
テーブルに単に user_id
列を配置することはできません。これは、役割が単一のユーザーにのみ属することを意味します。複数のユーザーに役割を割り当てるためには、role_user
テーブルが必要です。関係のテーブル構造を以下のようにまとめることができます:
users
id - integer
name - string
roles
id - integer
name - string
role_user
user_id - integer
role_id - integer
モデル構造
多対多の関係は、belongsToMany
メソッドの結果を返すメソッドを記述することで定義されます。belongsToMany
メソッドは、すべてのアプリケーションの Eloquent モデルで使用される Illuminate\Database\Eloquent\Model
ベースクラスによって提供されます。たとえば、User
モデルに roles
メソッドを定義しましょう。このメソッドに渡される最初の引数は関連するモデルクラスの名前です:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class User extends Model
{
/**
* The roles that belong to the user.
*/
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
}
関係が定義されると、roles
ダイナミック関係プロパティを使用してユーザーの役割にアクセスできます:
use App\Models\User;
$user = User::find(1);
foreach ($user->roles as $role) {
// ...
}
すべての関係はクエリビルダーとしても機能するため、roles
メソッドを呼び出してクエリにさらなる制約を追加し、クエリに条件を追加することができます。```
$roles = User::find(1)->roles()->orderBy('name')->get();
リレーションの中間テーブルのテーブル名を決定するために、Eloquent は関連する2つのモデル名をアルファベット順に結合します。ただし、この規則を上書きすることもできます。belongsToMany
メソッドに2番目の引数を渡すことで、この規則を上書きすることができます。
return $this->belongsToMany(Role::class, 'role_user');
中間テーブルの名前をカスタマイズするだけでなく、belongsToMany
メソッドに追加の引数を渡すことで、テーブル上のキーの列名をカスタマイズすることもできます。3番目の引数は、リレーションを定義しているモデルの外部キー名であり、4番目の引数は結合先のモデルの外部キー名です。
return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');
リレーションの逆を定義する
多対多のリレーションの「逆」を定義するには、関連するモデルで、belongsToMany
メソッドの結果を返すメソッドを定義する必要があります。ユーザー/ロールの例を完成させるために、Role
モデルで users
メソッドを定義しましょう。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Role extends Model
{
/**
* The users that belong to the role.
*/
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
}
関係は、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');