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

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メソッドを呼び出すときにどのコメントを取得するのかという制約を追加でき、クエリに条件を続けてチェーンでつなげます。

$comment = 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はリレーションメソッドの名前として、_に続けて主キーのカラム名をサフィックスとして付けた名前をデフォルトの外部キーとします。しかし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クラス、一方のカスタムポリモーフィック多対多ピボットモデルは、Illuminate\Database\Eloquent\Relations\MorphPivotクラスを拡張する必要があります。例として、カスタムRoleUserピボットモデルを使用する、Roleを定義してみましょう。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

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

<?php

namespace App;

use Illuminate\Database\Eloquent\Relations\Pivot;

class RoleUser extends Pivot
{
    //
}

中間テーブルからカラムを取得するために、usingwithPivotを組み合わせられます。たとえば、withPivotメソッドにカラム名を渡すことにより、RoleUserピボットテーブルからcreated_byupdated_byカラムを取得してみましょう。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

注意: ピボットモデルでは、SoftDeletesトレイトを使わないほうが良いでしょう。ピボットレコードのソフト削除が必要な場合は、ピボットモデルを実際のEloquentモデルに変換することを考えてください。

カスタム中間テーブルとIDの増分

カスタム中間テーブルを使用し、他対多リレーションを定義しており、その中間テーブルが自動増加する主キーを持つ場合、カスタム中間テーブルクラスのincrementingプロパティをtrueにセットしてください。

/**
 * IDの自動増加
 *
 * @var bool
 */
public $incrementing = true;

Has One Through

"has-one-through"(〜経由で1つへ紐づく)リレーションは、一つの仲介関係を通し、モデルを関連付けます。 たとえば、各サプライヤは一人のユーザーを持ち、各ユーザーは一つのユーザー履歴レコードに関連付けられているとすると、サプライヤモデルは、ユーザーを経由してユーザー履歴にアクセスできます。このリレーションを定義するために必要な、データベーステーブルを見てみましょう。

users
    id - integer
    supplier_id - integer

suppliers
    id - integer

history
    id - integer
    user_id - integer

historyテーブルにはsupplier_idカラムが含まれていませんが、hasOneThroughリレーションでサプライヤモデルからユーザー履歴へアクセスできます。このリレーションのテーブル構造を確認できたので、Supplierモデルを定義しましょう。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Supplier extends Model
{
    /**
     * ユーザー履歴の取得
     */
    public function userHistory()
    {
        return $this->hasOneThrough('App\History', 'App\User');
    }
}

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

通常、Eloquent外部キー規約は、リレーションのクエリを実行するときに使用されます。リレーションのキーをカスタマイズしたい場合は、hasOneThroughメソッドの第3引数、第4引数に渡します。第3引数は仲介モデルの外部キー名です。第4引数は最終モデルの外部キーの名前です。第5引数はローカルキー、第6引数は仲介モデルのローカルキーです。

class Supplier extends Model
{
    /**
     * ユーザー履歴の取得
     */
    public function userHistory()
    {
        return $this->hasOneThrough(
            'App\History',
            'App\User',
            'supplier_id', // usersテーブルの外部キー
            'user_id', // historyテーブルの外部キー
            'id', // suppliersテーブルのローカルキー
            'id' // usersテーブルのローカルキー
        );
    }
}

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:多様性)リレーションは一つの関係で、対象となるモデルを複数のモデルに所属させます。

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

テーブル構造

1対1ポリモーフィックリレーションは、1対1リレーションと似ています。しかしながら一つの関連付けで、対象モデルが複数のタイプのモデルに所属できる点が異なります。たとえば、ブログのPostUserが、Imageモデルに対してポリモーフィックリレーションを共有しているとしましょう。1対1ポリモーフィックリレーションを使えば、1つでブログポストとユーザーアカウント両方に対し使用できる、画像のリストを利用できます。最初に、テーブル構造を見てみましょう。

posts
    id - integer
    name - string

users
    id - integer
    name - string

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

imageable_idimageable_typeカラムに注目してください。imageable_idカラムは、ポストかユーザーのID値を含みます。一方のimageable_typeカラムは、親モデルのクラス名を含みます。imageable関係がアクセスされた場合に、Eloquentによりimageable_typeカラムは親のモデルがどんな「タイプ」であるかを決めるために使用されます。

モデル構造

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

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

class Post extends Model
{
    /**
     * ポストのイメージを所得
     */
    public function image()
    {
        return $this->morphOne('App\Image', 'imageable');
    }
}

class User extends Model
{
    /**
     * ユーザーのイメージを所得。
     */
    public function image()
    {
        return $this->morphOne('App\Image', 'imageable');
    }
}

リレーションの取得

データベーステーブルとモデルが定義できたら、モデルを使いリレーションへアクセスできます。例として、ポストに対するイメージを取得してみましょう。image動的プロパティが使用できます。

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

$image = $post->image;

morphToを実行する名前のメソッドでアクセスすることで、ポリモーフィックモデルから親を取得することもできます。この場合、Imageモデル上のimageableメソッドです。では、このメソッドを動的プロパティとして呼び出してみましょう。

$image = App\Image::find(1);

$imageable = $image->imageable;

Imageモデル上のimageableリレーションは、そのイメージを所有しているモデルのタイプにより、PostUserインスタンスのどちらかを返します。

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

テーブル構造

1対多ポリモーフィックリレーションは、シンプルな1対多リレーションと似ています。しかしながら一つの関連付けで、対象モデルが複数のタイプのモデルに所属できる点が異なります。たとえば、アプリケーションのユーザーが、ポストと動画の両方に「コメント」できるようにすると想像してください。ポリモーフィックリレーションを使用すれば、両方のシナリオに対し一つのcommentsテーブルで対処できます。最初に、このリレーションを構築するために必要な、テーブル構造を確認しましょう。

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;

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インスタンスのどちらかを返します。そのコメントを所有しているモデルのタイプにより決まります。

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

テーブル構造

多対多ポリモーフィックリレーションは、morphOnemorphManyリレーションより、少々複雑です。例として、ブログの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) {
    //
}

カスタムポリモーフィックタイプ

関連付けたモデルのタイプを保存するため、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関数で登録できますし、お望みであれば独立したサービスプロバイダを作成し、その中で行うこともできます。

Note: 既存のアプリケーションへ"morph map"を追加する場合は、データベース中のすべてのmorphable *_typeカラムの値は完全なクラス名を持っており、"map"名へ変換する必要が起きます。

リレーションのクエリ

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

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

リレーションにorWhereを続ける

前記の例では、リレーションのクエリ時に制約を自由に追加できることをデモンストレーションしました。しかし、orWhere節はリレーション制約として、同じレベルの論理グループにしてしまうため、使用には注意が必要です。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

use Illuminate\Database\Eloquent\Builder;

// Retrieve posts with at least one comment containing words like foo%...
$posts = App\Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'foo%');
})->get();

// Retrieve posts with at least ten comments containing words like foo%...
$posts = App\Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'foo%');
}, '>=', 10)->get();

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

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

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

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

use Illuminate\Database\Eloquent\Builder;

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

「ドット」記法を使い、ネストしたリレーションに対してクエリを実行できます。たとえば、以下のクエリにより、アカウントを無効(ban)されていない著者の、コメントが存在するすべてのポストを取得できます。

use Illuminate\Database\Eloquent\Builder;

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

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

既存のMorphToリレーションへクエリするには、whereHasMorphとこれに対応するメソッドを利用してください。

use Illuminate\Database\Eloquent\Builder;

// ポストとビデオと関連付いているコメントをtitle like foo%で取得する
$comments = App\Comment::whereHasMorph(
    'commentable',
    ['App\Post', 'App\Video'],
    function (Builder $query) {
        $query->where('title', 'like', 'foo%');
    }
)->get();

// ポストとビデオと関連付いているコメントをtitle not like foo%で取得する
$comments = App\Comment::whereDoesntHaveMorph(
    'commentable',
    'App\Post',
    function (Builder $query) {
        $query->where('title', 'like', 'foo%');
    }
)->get();

関連するモデルに応じて異なった制約を追加するために、$typeパラメータを使用できます。

use Illuminate\Database\Eloquent\Builder;

$comments = App\Comment::whereHasMorph(
    'commentable',
    ['App\Post', 'App\Video'],
    function (Builder $query, $type) {
        $query->where('title', 'like', 'foo%');

        if ($type === 'App\Post') {
            $query->orWhere('content', 'like', 'foo%');
        }
    }
)->get();

ポリモーフィックモデルの配列を渡す代わりに*をワイルドカードとして指定でき、その場合はデータベースからすべてのポリモーフィックタイプをLaravelは取得します。

use Illuminate\Database\Eloquent\Builder;

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

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

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

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

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

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

use Illuminate\Database\Eloquent\Builder;

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

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

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

use Illuminate\Database\Eloquent\Builder;

$posts = App\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;

select文にwithCountを組み合わせる場合は、select文の後でwithCountを呼び出してください。

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

echo $posts[0]->title;
echo $posts[0]->body;
echo $posts[0]->comments_count;

Eagerロード

プロパティとしてEloquentリレーションにアクセスする場合、そのリレーションデータは「遅延ロード」されます。つまり、そのリレーションデータが最初にアクセスされるまで、実際にはロードされません。しかし、Eloquentでは、親のモデルに対するクエリと同時にリレーションを「Eagerロード」可能です。EagerロードはN+1クエリ問題の解決策です。N+1クエリ問題を理解するために、Authorと関連しているBookモデルを考えてみてください。

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

morphToリレーションのネストしたEagerロード

リレーションにより返される様々なエンティティのリレーションがネストしている、morphToリレーションをEagerロードしたい場合は、withメソッドをmorphToリレーションのmorphWithメソッドと組み合わせて使用します。

<?php

use Illuminate\Database\Eloquent\Model;

class ActivityFeed extends Model
{
    /**
     * アクティビティフィードレコードの親を取得
     */
    public function parentable()
    {
        return $this->morphTo();
    }
}

この例の場合、EventPhotoPostモデルでActivityFeedモデルを構成していると仮定しましょう。さらに、EventモデルはCalendarモデルへ所属し、PhotoモデルはTagモデルと関連し、PostモデルはAuthorモデルへ所属しているとしましょう。

このモデル定義とリレーションを使用して、ActivityFeedモデルインスタンスを取得し、全parentableモデルとそれぞれのネストしたリレーションをEagerロードできます。

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

特定カラムのEagerロード

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

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

Note: この機能を使用する場合はidカラムと、取得するカラムリスト中の関連付けられた外部キーカラムすべてを常に含める必要があります。

デフォルトのEagerロード

あるモデル取得時、常にリレーションを取得したい場合もあります。そのためには、モデルに$withプロパティを定義します。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    /**
     * 常にロードするリレーション
     *
     * @var array
     */
    protected $with = ['author'];

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

$withプロパティで指定したリレーションを一つのクエリで削除したい場合は、withoutメソッドを使用します。

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

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

Note: limittakeクエリビルダメソッドは、Eagerロードの制約時には使用できません。

遅延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
    ];
}

ネストした遅延EagerローディングとmorphTo

morphToリレーションをEagerロードし、同時にこのリレーションが数多くのエンティティとネストしたリレーションを返す場合、loadMorphメソッドを使用してください。

このメソッドは第1引数として、morphToリレーション名を受け取り、第2引数にはモデル/リレーションのペアの配列を引き受けます。このメソッドを理解するため、次のモデルを考えてください。

<?php

use Illuminate\Database\Eloquent\Model;

class ActivityFeed extends Model
{
    /**
     * アクティビティ・フィード・レコードの親の取得
     */
    public function parentable()
    {
        return $this->morphTo();
    }
}

この例で、EventPhotoPostモデルがActivityFeedモデルにより生成されると仮定してください。さらに、EventモデルはCalendarモデルへ所属、PhotoモデルはTagモデルへ所属、PostモデルはAuthorモデルへ所属していると想定してください。

これらのモデルとリレーションを使用し、ActivityFeedモデルインスタンスと、Eagerロードされた全parentableモデル、各個にネストしたリレーションを取得しましょう。

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

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

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.']),
]);

モデルとリレーションの再帰的保存

モデルと関連付いているリレーション全てを保存(save)したい場合は、pushメソッドを使用してください。

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

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

$post->push();

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.',
    ],
]);

リレーション上のモデルを作成、変更するために、findOrNewfirstOrNewfirstOrCreateupdateOrCreateメソッドも使用できます。

Belongs To関係

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

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

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

$user->save();

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

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

$user->save();

デフォルトモデル

belongsTohasOnehasOneThroughmorphOneリレーションでは、指定したリレーションが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, $post) {
        $user->name = 'Guest Author';
    });
}

多対多関係

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

ドキュメント章別ページ

ヘッダー項目移動

注目:アイコン:ページ内リンク設置(リンクがないヘッダーへの移動では、リンクがある以前のヘッダーのハッシュをURLへ付加します。

移動

クリックで即時移動します。

設定

適用ボタンクリック後に、全項目まとめて適用されます。

カラーテーマ
和文指定 Pagination
和文指定 Scaffold
Largeスクリーン表示幅
インデント
本文フォント
コードフォント
フォント適用確認

フォントの指定フィールドから、フォーカスが外れると、当ブロックの内容に反映されます。EnglishのDisplayもPreviewしてください。

フォント設定時、表示に不具合が出た場合、当サイトのクッキーを削除してください。

バックスラッシュを含むインライン\Code\Blockの例です。

以下はコードブロックの例です。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

設定を保存する前に、表示が乱れないか必ず確認してください。CSSによるフォントファミリー指定の知識がない場合は、フォントを変更しないほうが良いでしょう。

キーボード・ショートカット

オープン操作

PDC

ページ(章)移動の左オフキャンバスオープン

HA

ヘッダー移動モーダルオープン

MS

移動/設定の右オフキャンバスオープン

ヘッダー移動

T

最初のヘッダーへ移動

E

最後のヘッダーへ移動

NJ

次ヘッダー(H2〜H4)へ移動

BK

前ヘッダー(H2〜H4)へ移動

その他

?

このヘルプページ表示
閉じる