Laravel5.5 LTS 日本語ドキュメント

このリポジトリはPHP Webアプリフレームワークである、LaravelのLTSバージョンである5.5の公式英文ドキュメントを日本語へ翻訳しています。

This project is maintained by okinaka

Eloquent:リレーション

イントロダクション

データベーステーブルは大抵の場合他のものと関連しています。たとえばブログ投稿(ポスト)は多くのコメントを持つか、それを投稿したユーザーと関連しています。Eloquentはそうしたリレーションを簡単に管理し操作できるようにするとともに、様々なタイプのリレーションをサポートしています。

リレーションの定義

Eloquentのリレーション(関係)とはEloquentモデルクラスのメソッドとして定義します。Eloquentモデル自身と同様にリレーションはパワフルなクエリビルダとして動作しますので、メソッドとして定義しているリレーションはパワフルなメソッドのチェーンとクエリ能力を提供できるのです。例として、posts関係に追加の制約をチェーンしてみましょう。

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

リレーションの詳しい使い方へ進む前に、リレーションの各タイプをどのように定義するかを学びましょう。

1対1

1対1関係が基本です。たとえばUserモデルはPhoneモデル一つと関係しているとしましょう。このリレーションを定義するには、phoneメソッドをUserモデルに設置します。phoneメソッドはベースのEloquentモデルクラスのhasOneメソッドを結果として返す必要があります。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * ユーザーに関連する電話レコードを取得
     */
    public function phone()
    {
        return $this->hasOne('App\Phone');
    }
}

hasOneメソッドの最初の引数は関係するモデルの名前です。リレーションが定義できたらEloquentの動的プロパティを使って、関係したレコードを取得できます。動的プロパティによりモデル上のプロパティのようにリレーションメソッドにアクセスできます。

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

Eloquentはリレーションの外部キーがモデル名に基づいていると仮定します。この場合自動的にPhoneモデルはuser_id外部キーを持っていると仮定します。この規約をオーバーライドしたければ、hasOneメソッドの第2引数を指定してください。

return $this->hasOne('App\Phone', 'foreign_key');

Eloquentは親のidカラム(もしくはカスタム$primaryKey)と一致する外部キーの値を持っていると仮定します。言い換えればEloquentはユーザーのidカラムの値をPhoneレコードのuser_idカラムに存在しないか探します。リレーションで他のidを使いたければ、hadOneメソッドの第3引数でカスタムキーを指定してください。

return $this->hasOne('App\Phone', 'foreign_key', 'local_key');

逆の関係の定義

これでUserからPhoneモデルへアクセスできるようになりました。今度はPhoneモデルからそれを所有しているUserへアクセスするリレーションを定義しましょう。hasOneの逆のリレーションを定義するには、belongsToメソッドを使います。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Phone extends Model
{
    /**
     * この電話を所有するUserを取得
     */
    public function user()
    {
        return $this->belongsTo('App\User');
    }
}

上の例でEloquentはPhoneモデルのuser_idに一致するidを持つUserモデルを見つけようとします。Eloquentはリレーションメソッド名に_idのサフィックスを付けた名前をデフォルトの外部キー名とします。しかしPhoneモデルの外部キーがuser_idでなければ、belongsToメソッドの第2引数にカスタムキー名を渡してください。

/**
 * この電話を所有するUserを取得
 */
public function user()
{
    return $this->belongsTo('App\User', 'foreign_key');
}

親のモデルの主キーがidでない、もしくは子のモデルと違ったカラムで紐付けたい場合は、親テーブルのカスタムキー名をbelongsToメソッドの第3引数に渡してください。

/**
 * この電話を所有するUserを取得
 */
public function user()
{
    return $this->belongsTo('App\User', 'foreign_key', 'other_key');
}

デフォルトモデル

belongsToリレーションでは、指定したリレーションがnullの場合に返却するデフォルトモデルを定義できます。このパターンは、頻繁にNullオブジェクトパターンと呼ばれ、コードから条件のチェックを省くのに役立ちます。以下の例では、ポストに従属するuserがない場合に、空のApp\Userモデルを返しています。

/**
 * ポストの著者を取得
 */
public function user()
{
    return $this->belongsTo('App\User')->withDefault();
}

属性を指定したデフォルトモデルを返すためには、withDefaultメソッドに配列かクロージャを渡してください。

/**
 * ポストの著者を取得
 */
public function user()
{
    return $this->belongsTo('App\User')->withDefault([
        'name' => 'Guest Author',
    ]);
}

/**
 * ポストの著者を取得
 */
public function user()
{
    return $this->belongsTo('App\User')->withDefault(function ($user) {
        $user->name = 'Guest Author';
    });
}

1対多

「1対多」リレーションは一つのモデルが他の多くのモデルを所有する関係を定義するために使います。ブログポストが多くのコメントを持つのが一例です。他のEloquentリレーションと同様に1対多リレーションはEloquentモデルの関数として定義します。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * ブログポストのコメントを取得
     */
    public function comments()
    {
        return $this->hasMany('App\Comment');
    }
}

Eloquentは、Commentモデルに対する外部キーを自動的に決めることを心に留めてください。規約によりEloquentは、自分自身のモデル名の「スネークケース」に_idのサフィックスをつけた名前と想定します。ですから今回の例でEloquentは、Commentモデルの外部キーをpost_idであると想定します。

リレーションを定義したら、commentsプロパティによりコメントのコレクションへアクセスできます。Eloquentは「動的プロパティ」を提供しているので、モデルのプロパティとして定義したリレーションメソッドへアクセスできることを覚えておきましょう。

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

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

もちろん、全リレーションはクエリビルダとしても働きますから、commentsメソッドを呼び出すときにどのコメントを取得するのかという制約を追加でき、クエリに条件を続けてチェーンでつなげます。

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

hasOneメソッドと同様に、外部キーとローカルキーをhasManyメソッドに追加の引数として渡すことでオーバーライドできます。

return $this->hasMany('App\Comment', 'foreign_key');

return $this->hasMany('App\Comment', 'foreign_key', 'local_key');

1対多 (Inverse)

これでポストの全コメントにアクセスできます。今度はコメントから親のポストへアクセスできるようにしましょう。hasManyリレーションの逆を定義するには子のモデルでbelongsToメソッドによりリレーション関数を定義します。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * このコメントを所有するポストを取得
     */
    public function post()
    {
        return $this->belongsTo('App\Post');
    }
}

リレーションが定義できたらCommentPostモデルをpost動的プロパティにより取得しましょう。

$comment = App\Comment::find(1);

echo $comment->post->title;

前例でEloquentはCommentモデルのpost_idと一致するidPostモデルを見つけようとします。Eloquentはリレーションメソッドの名前に_idのサフィックスをつけた名前をデフォルトの外部キーとします。しかしCommentモデルの外部キーがpost_idでなければ、belongsToメソッドの第2引数にカスタムキー名を指定してください。

/**
 * このコメントを所有するポストを取得
 */
public function post()
{
    return $this->belongsTo('App\Post', 'foreign_key');
}

親のモデルの主キーがidでない、もしくは子のモデルと違ったカラムで紐付けたい場合は、親テーブルのカスタムキー名をbelongsToメソッドの第3引数に渡してください。

/**
 * このコメントを所有するポストを取得
 */
public function post()
{
    return $this->belongsTo('App\Post', 'foreign_key', 'other_key');
}

多対多

多対多の関係はhasOnehasManyリレーションよりも多少複雑な関係です。このような関係として、ユーザー(user)が多くの役目(roles)を持ち、役目(role)も大勢のユーザー(users)に共有されるという例が挙げられます。たとえば多くのユーザーは”管理者”の役目を持っています。usersrolesrole_userの3テーブルがこの関係には必要です。role_userテーブルは関係するモデル名をアルファベット順に並べたもので、user_idrole_idを持つ必要があります。

多対多リレーションはbelongsToManyメソッド呼び出しを記述することで定義します。例としてUserモデルにrolesメソッドを定義してみましょう。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * userに所属する役目を取得
     */
    public function roles()
    {
        return $this->belongsToMany('App\Role');
    }
}

リレーションが定義できたら、roles動的プロパティを使いユーザーの役割にアクセスできます。

$user = App\User::find(1);

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

もちろん他のリレーションタイプと同様にリレーションを制約するクエリをrolesに続けてチェーンすることができます。

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

前に述べたようにリレーションの結合テーブルの名前を決めるため、Eloquentは2つのモデル名をアルファベット順に結合します。しかしこの規約は自由にオーバーライドできます。belongsToManyメソッドの第2引数に渡してください。

return $this->belongsToMany('App\Role', 'role_user');

結合テーブル名のカスタマイズに加えテーブルのキーカラム名をカスタマイズするには、belongsToManyメソッドに追加の引数を渡してください。第3引数はリレーションを定義しているモデルの外部キー名で、一方の第4引数には結合するモデルの外部キー名を渡します。

return $this->belongsToMany('App\Role', 'role_user', 'user_id', 'role_id');

逆の関係の定義

多対多のリレーションの逆リレーションを定義するには、関連するモデルでもbelongsToManyを呼び出してください。引き続きユーザーと役割の例を続けますがRoleモデルでusersメソッドを定義してみましょう。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    /**
     * 役目を所有するユーザー
     */
    public function users()
    {
        return $this->belongsToMany('App\User');
    }
}

ご覧の通り一方のUserと全く同じ定義のリレーションです。違いはApp\Userモデルを参照していことです。同じbelongsToManyメソッドを使っているのですから、通常のテーブル名、キーカスタマイズのオプションは逆の多対多リレーションを定義するときでも全て使用できます。

中間テーブルのカラム取得

既に学んだように、多対多リレーションの操作には中間テーブルが必要です。Eloquentこのテーブルを操作する便利な手段を用意しています。例としてUserオブジェクトが関連するRoleオブジェクトを持っているとしましょう。このリレーションへアクセスした後、モデルのpivot属性を使い中間テーブルにアクセスできます。

$user = App\User::find(1);

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

取得したそれぞれのRoleモデルはpivot属性と自動的に結合されます。この属性は中間テーブルを表すモデルを含んでおり、他のElouquentモデルと同様に使用できます。

デフォルトでモデルキーはpivotオブジェクト上のものを表しています。中間テーブルがその他の属性を持っている場合、リレーションを定義するときに指定できます。

return $this->belongsToMany('App\Role')->withPivot('column1', 'column2');

もし中間テーブルのcreated_atupdated_atタイムスタンプを自動的に保守したい場合は、withTimestampsメソッドをリレーション定義に付けてください。

return $this->belongsToMany('App\Role')->withTimestamps();

pivot属性の名前変更

前述の通り、中間テーブルにはpivot属性を使ってアクセスできます。その際、アプリケーションの目的をより良く反映するためにpivot属性の名前を変更することができます。

たとえばユーザーがポッドキャストを購読するようなアプリケーションでは、ユーザーとポッドキャストが多対多の関係となっていることがあります。その場合、中間テーブルへアクセスする際のpivot属性の名前をsubscriptionに変更したいかもしれません。これはリレーションを定義する際に、asメソッドを使うことで実現できます。

return $this->belongsToMany('App\Podcast')
                ->as('subscription')
                ->withTimestamps();

これにより、変更した名前で中間テーブルへアクセスできます。

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

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

中間テーブルのカラムを使った関係のフィルタリング

リレーション定義時に、wherePivotwherePivotInを使い、belongsToManyが返す結果をフィルタリングすることも可能です。

return $this->belongsToMany('App\Role')->wherePivot('approved', 1);

return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);

カスタム中間テーブルモデルの定義

リレーションの中間テーブルを表すカスタムモデルを定義したい場合は、リレーションの定義時にusingメソッドを呼び出します。リレーションの中間テーブルを表すために使用されるカスタムモデルは全て、Illuminate\Database\Eloquent\Relations\Pivotクラスを拡張する必要があります。例として、カスタムUserRole中間モデルを利用する、Roleを定義してみましょう。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    /**
     * 役目を所有するユーザー
     */
    public function users()
    {
        return $this->belongsToMany('App\User')->using('App\UserRole');
    }
}

UserRole定義時に、Pivotクラスを拡張します。

<?php

namespace App;

use Illuminate\Database\Eloquent\Relations\Pivot;

class UserRole extends Pivot
{
    //
}

Has Many Through

has many through(〜経由の多対多)リレーションは、仲介するテーブルを通して直接関連付けしていないテーブルへアクセスするための、便利な近道を提供します。たとえばCountryモデルはUsersモデルを経由して、多くのPostsを所有することでしょう。テーブルは以下のような構成になります。

countries
    id - integer
    name - string

users
    id - integer
    country_id - integer
    name - string

posts
    id - integer
    user_id - integer
    title - string

たとえpostsテーブルにcountry_idが存在しなくても、hasManyThroughリレーションではCountryのPostへ$country->postsによりアクセスできます。このクエリを行うためにEloquentは仲介するusersテーブルのcountry_idを調べます。一致するユーザーIDが存在していたらpostsテーブルのクエリに利用します。

ではリレーションのテーブル構造を理解したところで、Countryモデルを定義しましょう。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Country extends Model
{
    /**
     * この国の全ポストを取得
     */
    public function posts()
    {
        return $this->hasManyThrough('App\Post', 'App\User');
    }
}

hasManyThroughメソッドの第一引数は最終的にアクセスしたいモデル名で、第2引数は仲介するモデル名です。

リレーションのクエリ実行時は、典型的なEloquentの外部キー規約が使用されます。リレーションのキーをカスタマイズしたい場合は、hasManyThroughメソッドの第3引数と、第4引数を指定してください。第3引数は仲介モデルの外部キー名、第4引数は最終的なモデルの外部キー名です。第5引数はローカルキーで、第6引数は仲介モデルのローカルキーです。

class Country extends Model
{
    public function posts()
    {
        return $this->hasManyThrough(
            'App\Post',
            'App\User',
            'country_id', // usersテーブルの外部キー
            'user_id', // postsテーブルの外部キー
            'id', // countriesテーブルのローカルキー
            'id' // usersテーブルのローカルキー
        );
    }
}

ポリモーフィック関係

テーブル構造

ポリモーフィック(Polymorphic:多様性)リレーションは一つの関係で、複数のモデルに所属させます。たとえば、アプリケーションのユーザーがポスト(post:記事)と動画(video)に「コメント(comment)」できるとしましょう。最初にこのリレーションを構築するために必要な構造を確認してください。

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

commentsテーブルには2つの重要なcommentable_idcommentable_typeカラムがあります。commentable_idカラムはポストかビデオのID値を保持します。一方のcommentable_typeカラムには、所有しているモデルのクラス名が保存されます。commentable関係にアクセスされた時に、所有してるのはどちらの「タイプ」のモデルなのか、ORMが決めるためにcommentable_typeカラムが存在しています。

モデル構造

次にこのリレーションを構築するために必要なモデル定義を見てみましょう。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * 所有しているcommentableモデルの全取得
     */
    public function commentable()
    {
        return $this->morphTo();
    }
}

class Post extends Model
{
    /**
     * 全ポストコメントの取得
     */
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }
}

class Video extends Model
{
    /**
     * 全ビデオコメントの取得
     */
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }
}

ポリモーフィックリレーションの取得

データベーステーブルとモデルが定義できたら、モデルを使いリレーションにアクセスできます。たとえば、あるポストの全コメントへアクセスするには、comments動的プロパティを使うだけです。

$post = App\Post::find(1);

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

morphToの呼び出しを行うメソッドの名前にアクセスすることにより、ポリモーフィック関連の所有者を取得することもできます。この例の場合、Commentモデルのcommentableメソッドです。では、このメソッドに動的プロパティによりアクセスしましょう。

$comment = App\Comment::find(1);

$commentable = $comment->commentable;

Commentモデルのcommentable関係は、Postvideoインスタンスのどちらかを返します。そのコメントを所有しているモデルのタイプにより決まります。

カスタムポリモーフィックリレーション

関連付けられたモデルのタイプを保存するため、デフォルトでLaravelははっきりと識別できるクラス名を使います。たとえば上記の例で、CommentPostVideoに所属しているとすると、commentable_typeはデフォルトでApp\PostApp\Videoのどちらかになるでしょう。しかし、データーベースをアプリケーションの内部構造と分離したい場合もあります。その場合、リレーションの”morph map”を定義し、クラス名の代わりに使用する、各モデルに関連づいたテーブル名をEloquentへ指示することができます。

use Illuminate\Database\Eloquent\Relations\Relation;

Relation::morphMap([
    'posts' => 'App\Post',
    'videos' => 'App\Video',
]);

morphMapは、AppServiceProviderboot関数で登録できますし、お望みであれば独立したサービスプロバイダを作成し、その中で行うこともできます。

ポリモーフィック関係の多対多

テーブル構造

伝統的なポリモーフィックリレーションに加え、「多対多」のポリモーフィックリレーションも指定することができます。たとえばブログのPostVideoモデルは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

モデル構造

次にモデルにその関係を用意しましょう。PostVideoモデルは両方ともベースのEloquentクラスのmorphToManyメソッドを呼び出すtagsメソッドを持っています。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * ポストに対する全タグを取得
     */
    public function tags()
    {
        return $this->morphToMany('App\Tag', 'taggable');
    }
}

逆の関係の定義

次にTagモデルで関係する各モデルに対するメソッドを定義する必要があります。たとえばこの例であれば、postsメソッドとvideosメソッドを用意します。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
    /**
     * このタグをつけた全ポストの取得
     */
    public function posts()
    {
        return $this->morphedByMany('App\Post', 'taggable');
    }

    /**
     * このタグをつけた全ビデオの取得
     */
    public function videos()
    {
        return $this->morphedByMany('App\Video', 'taggable');
    }
}

リレーションの取得

データベーステーブルとモデルが定義できたら、モデルを使いリレーションにアクセスできます。たとえばポストに対する全タグへアクセスするには、単にtags動的プロパティを使用するだけです。

$post = App\Post::find(1);

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

さらにmorphedByManyを呼び出すメソッドの名前にアクセスし、ポリモーフィックモデルからポリモーフィックリレーションの所有者を取得することも可能です。この例の場合Tagモデルのpostsvideosメソッドです。では動的プロパティとしてメソッドを呼び出しましょう。

$tag = App\Tag::find(1);

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

リレーションのクエリ

Eloquentリレーションは全てメソッドとして定義されているため、リレーションのクエリを実際に記述しなくても、メソッドを呼び出すことで、そのリレーションのインスタンスを取得できます。さらに、すべてのタイプのEloquentリレーションもクエリビルダとしても動作し、データベースに対してSQLが最終的に実行される前に、そのリレーションのクエリをチェーンで続けて記述できます。

たとえばブログシステムで関連した多くのPostモデルを持つUserモデルを想像してください。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * ユーザーの全ポストの取得
     */
    public function posts()
    {
        return $this->hasMany('App\Post');
    }
}

次のようにpostsリレーションのクエリに追加の制約を付け加えられます。

$user = App\User::find(1);

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

すべてのクエリビルダメソッドをリレーションで使用することも可能です。ですから、提供している全メソッドを学ぶために、クエリビルダのドキュメントを研究してください。

リレーションメソッド 対 動的プロパティ

Eloquentリレーションクエリに追加の制約を加える必要がなければ、シンプルにそのリレーションへプロパティとしてアクセスできます。UserPostの例を続けるとして、ユーザーの全ポストには次のようにアクセスできます。

$user = App\User::find(1);

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

動的プロパティは「遅延ロード」されます。つまり実際にアクセスされた時にだけそのリレーションのデータはロードされます。そのため開発者は多くの場合にEagerローディングを使い、モデルをロードした後にアクセスするリレーションを前もってロードしておきます。Eagerロードはモデルのリレーションをロードするため実行されるSQLクエリを大幅に減らしてくれます。

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

関連付けたモデルのレコードに基づいて、モデルのレコードに対するマッチングを絞り込みたい場合もあるでしょう。たとえば、最低でも一つのコメントを持つ、全ブログポストを取得したい場合を考えてください。これを行うためには、リレーションの名前をhasorHasメソッドに渡します。

// 最低1つのコメントを持つ全ポストの取得
$posts = App\Post::has('comments')->get();

演算子と数を指定しクエリをカスタマイズすることもできます。

// 3つ以上のコメントを持つ全ポストの取得
$posts = Post::has('comments', '>=', 3)->get();

ネストしたhas文は「ドット」記法で組立てられます。たとえば最低一つのコメントと評価を持つ全ポストを取得する場合です。

// 最低1つのコメントと、それに対する評価を持つ全ポストの取得
$posts = Post::has('comments.votes')->get();

もっと強力な機能がお望みならばhasの問い合わせに”WHERE”で条件をつけるられる、whereHasorWhereHasを利用して下さい。これらのメソッドによりリレーションの制約にカスタマイズした制約を追加できます。たとえばコメントの内容を調べることです。

// like foo%の制約に一致する最低1つのコメントを持つ全ポストの取得
$posts = Post::whereHas('comments', function ($query) {
    $query->where('content', 'like', 'foo%');
})->get();

存在しないリレーションのクエリ

モデルへアクセスする時に、結果をリレーションを持たないレコードに限定したい場合があります。たとえばブログで、コメントを持たないポストのみ全て取得したい場合です。これを行うには、doesntHaveorDoesntHaveメソッドにリレーション名を渡してください。

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

もっと強力な機能がお望みなら、doesntHaveクエリに”WHERE”で条件を付けられる、whereDoesntHaveorWhereDoesntHaveメソッドを使ってください。これらのメソッドはコメントの内容を調べるなど、リレーション制約にカスタム制約を付け加えられます。

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

関連するモデルのカウント

リレーション結果の件数を実際にレコードを読み込むことなく知りたい場合は、withCountメソッドを使います。件数は結果のモデルの{リレーション名}_countカラムに格納されます。

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

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

クエリによる制約を加え、複数のリレーションの件数を取得することも可能です。

$posts = Post::withCount(['votes', 'comments' => function ($query) {
    $query->where('content', 'like', 'foo%');
}])->get();

echo $posts[0]->votes_count;
echo $posts[0]->comments_count;

同じリレーションに複数の件数を含めるため、リレーション件数結果の別名も付けられます。

$posts = Post::withCount([
    'comments',
    'comments as pending_comments_count' => function ($query) {
        $query->where('approved', false);
    }
])->get();

echo $posts[0]->comments_count;

echo $posts[0]->pending_comments_count;

Eagerロード

Eloquentリレーションをプロパティとしてアクセスする場合、リレーションのデータは「遅延ロード」されます。つまりプロパティにアクセスされるまで実際にリレーションのデータはロードされることはありません。しかし、Eloquentでは親モデルにクエリする時点で「Eagerロード」できます。EagerローディングはN+1クエリ問題を軽減するために用意しています。たとえばBookモデルがAuthorモデルと関連していると考えてください。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    /**
     * この本を書いた著者を取得
     */
    public function author()
    {
        return $this->belongsTo('App\Author');
    }
}

では全書籍とその著者を取得しましょう。

$books = App\Book::all();

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

このループではまず全ての本をテーブルから取得するために1クエリ実行され、それから著者をそれぞれの本について取得します。ですから25冊あるならば、このループで26クエリが発生します。

ありがたいことにクエリの数を徹底的に減らすためにEagerローディングを使うことができます。withメソッドを使い指定してください。

$books = App\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, ...)

複数のリレーションに対するEagerロード

一回の操作で異なった複数のリレーションをEagerロードする必要がある場合もあります。その場合でも、ただwithメソッドに引数を追加で渡すだけです。

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

ネストしたEagerロード

ネストしたリレーションをEagerロードする場合は「ドット」記法が使用できます。例としてEloquent文で全著者と著者個人のコンタクトも全部Eagerロードしてみましょう。

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

特定カラムのEagerロード

検索しているリレーションの中で全てのカラムが必要とは限りません。そのため、Eloquentではリレーションの中で検索したいカラムを特定することができます。

$users = App\Book::with('author:id,name')->get();

{note} この機能を使う際には、 検索したいカラムと一緒に id カラムを含めなければなりません。

Eagerロードへの制約

場合によりリレーションをEagerロードしたいが、Eagerロードクエリに制約を追加したい場合があります。例を見てください。

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

この例でEloquentは、titleカラムの内容にfirstという言葉を含むポストのみをEagerロードしています。もちろんEagerロード操作を更にカスタマイズするために、他のクエリビルダを呼び出すこともできます。

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

遅延Eagerローディング

既に親のモデルを取得した後にリレーションをEagerロードする必要がある場合もあるでしょう。たとえば、どの関連しているモデルをロードするかを動的に決める場合に便利です。

$books = App\Book::all();

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

Eagerロードに追加の制約をかける必要があるなら、ロードしたい関連へ配列のキーを付け渡してください。配列地は、クエリインスタンスを受け取る「クロージャ」でなければなりません。

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

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

public function format(Book $book)
{
    $book->loadMissing('author');

    return [
        'name' => $book->name,
        'author' => $book->author->name
    ];
}

関連したモデルの挿入/更新

Saveメソッド

Eloquentは新しいモデルをリレーションに追加するために便利なメソッドを用意しています。たとえばPostモデルに新しいCommentを挿入する必要がある場合です。Commentpost_id属性を自分で設定する代わりに、リレーションのsaveメソッドで直接Commentを挿入できます。

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

$post = App\Post::find(1);

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

動的プロパティとしてcommentsリレーションにアクセスできない点には注意してください。代わりにリレーションの取得でcommentsメソッドを呼び出しています。saveメソッドは自動的に新しいCommentモデルのpost_idへ適した値を代入します。

複数の関連したモデルを保存する必要があるなら、saveManyメソッドを使用できます。

$post = App\Post::find(1);

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

Createメソッド

savesaveManyメソッドに加え、createメソッドも使用できます。属性の配列を引数に受け付け、モデルを作成しデータベースへ挿入します。savecreateの違いはsaveが完全なEloquentモデルを受け付けるのに対し、createは普通のPHPの「配列」を受け付ける点です。

$post = App\Post::find(1);

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

{tip} createメソッドを使用する前に属性の複数代入に関するドキュメントを読んでおいてください。

createManyメソッドで複数のリレーションモデルを生成することができます。

$post = App\Post::find(1);

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

Belongs To関係

belongsToリレーションを更新する場合はassociateメソッドを使います。このメソッドは子モデルへ外部キーをセットします。

$account = App\Account::find(10);

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

$user->save();

belongsToリレーションを削除する場合はdissociateメソッドを使用します。このメソッドはリレーションの子モデルの外部キーをnullにします。

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

$user->save();

多対多関係

attach/detach

多対多リレーションを操作時により便利なように、Eloquentはヘルパメソッドをいくつか用意しています。例としてユーザーが多くの役割を持ち、役割も多くのユーザーを持てる場合を考えてみましょう。モデルを結びつけている中間テーブルにレコードを挿入することにより、ユーザーに役割を持たせるにはattachメソッドを使います。

$user = App\User::find(1);

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

モデルにリレーションを割りつけるときに中間テーブルに挿入したい追加のデータを配列で渡すこともできます。

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

もちろんユーザーから役割を削除する必要がある場合もあるでしょう。多対多リレーションのレコードを削除するにはdetachメソッドを使います。detachメソッドは中間テーブルから対応するレコードを削除します。しかし両方のモデルはデータベースに残ります。

// ユーザーから役割を一つ切り離す
$user->roles()->detach($roleId);

// ユーザーから役割を全部切り離す
$user->roles()->detach();

便利なようにattachdetachの両方共にIDの配列を指定することができます。

$user = App\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を削除したくない場合は、syncWithoutDetachingメソッドを使用します。

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

関連の切り替え

多対多リレーションは、指定したIDの関連状態を「切り替える」、toggleメソッドも提供しています。指定したIDが現在関連している場合は、関連を切り離します。同様に現在関連がない場合は、関連付けます。

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

中間テーブルへの追加データ保存

多対多リレーションを操作する場合、saveメソッドの第2引数へ追加の中間テーブルの属性を指定できます。

App\User::find(1)->roles()->save($role, ['expires' => $expires]);

中間テーブルのレコード更新

中間テーブルに存在している行を更新する必要がある場合は、updateExistingPivotメソッドを使います。このメソッドは、中間テーブルの外部キーと更新する属性の配列を引数に取ります。

$user = App\User::find(1);

$user->roles()->updateExistingPivot($roleId, $attributes);

親のタイムスタンプの更新

CommentPostに所属しているように、あるモデルが他のモデルに所属(belongsToもしくはbelongsToMany)しており、子のモデルが更新される時に親のタイムスタンプを更新できると便利なことがあります。たとえばCommentモデルが更新されたら、所有者のPostupdated_atタイムスタンプを自動的に”touch”したい場合です。Eloquentなら簡単です。子のモデルにtouchesプロパティを追加しリレーション名を指定してください。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * 全リレーションをtouch
     *
     * @var array
     */
    protected $touches = ['post'];

    /**
     * ポストに所属しているコメント取得
     */
    public function post()
    {
        return $this->belongsTo('App\Post');
    }
}

これでCommentが更新されると、所有しているPostupdated_atカラムも同時に更新され、これによりPostモデルのどの時点のキャッシュを無効にするか判定できます。

$comment = App\Comment::find(1);

$comment->text = 'Edit to this comment!';

$comment->save();