Laravel 8.x Eloquent:リレーション

イントロダクション

多くの場合、データベーステーブルは相互に関連(リレーション)しています。たとえば、ブログ投稿に多くのコメントが含まれている場合や、注文がそれを行ったユーザーと関連している場合があります。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;

class User extends Model
{
    /**
     * ユーザーに関連している電話の取得
     */
    public function phone()
    {
        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プロパティ以外の主キー値を使用する場合は、3番目の引数をhasOneメソッドに渡してください。

return $this->hasOne(Phone::class, 'foreign_key', 'local_key');

逆の関係の定義

UserモデルからPhoneモデルへアクセスできるようになりました。次に、電話の所有ユーザーへアクセスできるようにするPhoneモデルの関係を定義しましょう。belongsToメソッドを使用してhasOne関係の逆を定義できます。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Phone extends Model
{
    /**
     * この電話を所有しているユーザーの取得
     */
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

userメソッドを呼び出し時に、EloquentはPhoneモデルのuser_idカラムと一致するidを持つUserモデルを見つけようとします。

Eloquentは、リレーションメソッドの名前を調べ、メソッド名の末尾に_idを付けることにより、外部キー名を決定します。したがって、この場合、EloquentはPhoneモデルにuser_idカラムがあると想定します。ただし、Phoneモデルの外部キーがuser_idでない場合は、カスタムキー名をbelongsToメソッドの2番目の引数として渡してください。

/**
 * この電話を所有しているユーザーの取得
 */
public function user()
{
    return $this->belongsTo(User::class, 'foreign_key');
}

親モデルが主キーとしてidを使用しない場合、または別のカラムを使用して関連モデルを検索する場合は、belongsToメソッドへ親テーブルのカスタムキーを指定する3番目の引数を渡してください。

/**
 * この電話を所有しているユーザーの取得
 */
public function user()
{
    return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
}

1対多

1対多の関係は、単一のモデルが1つ以上の子モデルの親である関係を定義するために使用されます。たとえば、ブログ投稿はいくつもコメントを持つ場合があります。他のすべてのEloquent関係と同様に、1対多の関係はEloquentモデルでメソッドを定義することにより定義します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * ブログポストのコメントを取得
     */
    public function comments()
    {
        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');

1対多(逆)/所属

投稿のすべてのコメントへアクセスできるようになったので、今度はコメントからその親投稿へアクセスできるようにする関係を定義しましょう。hasMany関係の逆を定義するには、belongsToメソッドを呼び出す子モデルで関係メソッドを定義します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * コメントを所有している投稿を取得
     */
    public function post()
    {
        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番目の引数へ指定できます。

/**
 * コメントを所有している投稿を取得
 */
public function post()
{
    return $this->belongsTo(Post::class, 'foreign_key');
}

親モデルが主キーとしてidを使用していない場合、または別のカラムを使用して関連モデルを検索する場合は、belongsToメソッドへ親テーブルのカスタムキーを指定する3番目の引数を渡せます。

/**
 * コメントを所有している投稿を取得
 */
public function post()
{
    return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');
}

デフォルトモデル

belongsTohasOnehasOneThroughmorphOneリレーションを使用する場合、指定する関係がnullの場合に返すデフォルトモデルを定義できます。このパターンは、Nullオブジェクトパターンと呼ばれることが多く、コード内の条件付きチェックを省略するのに役立ちます。以下の例では、Postモデルにユーザーがアタッチされていない場合、userリレーションは空のApp\Models\Userモデルを返します。

/**
 * 投稿の作成者を取得
 */
public function user()
{
    return $this->belongsTo(User::class)->withDefault();
}

デフォルトモデルに属性を設定するには、配列またはクロージャをwithDefaultメソッドに渡します。

/**
 * 投稿の作成者を取得
 */
public function user()
{
    return $this->belongsTo(User::class)->withDefault([
        'name' => 'Guest Author',
    ]);
}

/**
 * 投稿の作成者を取得
 */
public function user()
{
    return $this->belongsTo(User::class)->withDefault(function ($user, $post) {
        $user->name = 'Guest Author';
    });
}

Has One Of Many

あるモデルが多くの関連モデルを持つことがありますが、そのリレーションにおける「最新」または「最も古い」関連モデルを簡単に取得したい場合があります。たとえば、Userモデルは多くのOrderモデルと関連しており、ユーザーが発注した最新の注文を操作する便利な方法を定義したいとします。これの実現には、hasOneのリレーションタイプとofManyメソッドを組み合わせて使います。

/**
 * ユーザーの最新注文の取得
 */
public function latestOrder()
{
    return $this->hasOne(Order::class)->latestOfMany();
}

同様に、あるリレーションシップの「最も古い」、つまり最初の関連モデルを取得するメソッドを定義することもできます。

/**
 * ユーザーの最も古い注文を取得
 */
public function oldestOrder()
{
    return $this->hasOne(Order::class)->oldestOfMany();
}

latestOfManyoldestOfManyメソッドはデフォルトで、ソート可能なモデルの主キーに基づいて、最新または最古の関連モデルを取得します。しかし、時には、別のソート基準を使って、より大きなリレーションシップから単一モデルを取得したい場合も起きるでしょう。

例えば、ofManyメソッドを使って、ユーザーの最も高い注文を取得することができます。ofManyメソッドは、ソート可能なカラムを第一引数として受け取り、関連するモデルを検索する際にどの集約関数(minまたはmax)を適用するかを指定します。

/**
 * ユーザーの一番高い注文の取得
 */
public function largestOrder()
{
    return $this->hasOne(Order::class)->ofMany('price', 'max');
}

Note: PostgreSQLはUUID列に対するMAX関数の実行をサポートしていないため、今のところPostgreSQLのUUIDカラムと組み合わせて1対多の関係を使用できません。

上級Has One Of Manyリレーション

より高度な"has one of many"リレーションを構築することも可能です。例えば、Productモデルは、新しい価格が公開された後でもシステム内で保持している、多くの関連Priceモデルを持つことができます。さらに、製品の新しい価格データは、published_atカラムにより、将来の日付で有効にするように事前に予約できることにしましょう。

要約すると、公開日が未来ではない最新の価格を取得する必要があるということです。さらに、2つの価格の公開日が同じであれば、より大きいIDを持つ価格を優先します。これを実現するには、最新の価格を決定するソート可能なカラムを含む配列を ofMany メソッドに渡す必要があります。さらに、ofManyメソッドの第2引数には、クロージャが渡されます。このクロージャは、リレーションシップクエリに追加の発行日制約を追加する役割を果たします。

/**
 * 製品の現在価格を取得
 */
public function currentPricing()
{
    return $this->hasOne(Price::class)->ofMany([
        'published_at' => 'max',
        'id' => 'max',
    ], function ($query) {
        $query->where('published_at', '<', now());
    });
}

Has One Through

"has-one-through"リレーションは、別のモデルとの1対1の関係を定義します。ただし、この関係は、3番目のモデルを仲介(through)に使うことで、宣言するモデルと別のモデルの1インスタンスとマッチさせることを意味します。

たとえば、自動車修理工場のアプリケーションでは、各「整備士(Mechanic)」モデルを1つの「自動車(Car)」モデルに関連付け、各「自動車」モデルを1つの「所有者(Owner)」モデルに関連付けることができます。整備士と所有者はデータベース内で直接の関係はありませんが、整備士は「車」モデルを介して所有者にアクセスできます。この関係を定義するために必要なテーブルを見てみましょう。

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;

class Mechanic extends Model
{
    /**
     * 車の所有者を取得
     */
    public function carOwner()
    {
        return $this->hasOneThrough(Owner::class, Car::class);
    }
}

hasOneThroughメソッドに渡す、最初の引数はアクセスする最終モデルの名前であり、2番目の引数は中間モデルの名前です。

キーの規約

リレーションのクエリ実行時は、一般的にEloquent外部キー規約を使用します。リレーションのキーをカスタマイズする場合は、それらを3番目と4番目の引数としてhasOneThroughメソッドに渡してください。3番目の引数は、中間モデルの外部キーの名前です。4番目の引数は、最終モデルの外部キーの名前です。5番目の引数はローカルキーであり、6番目の引数は中間モデルのローカルキーです。

class Mechanic extends Model
{
    /**
     * 車の所有者を取得
     */
    public function carOwner()
    {
        return $this->hasOneThrough(
            Owner::class,
            Car::class,
            'mechanic_id', // carsテーブルの外部キー
            'car_id', // ownersテーブルの外部キー
            'id', // mechanicsテーブルのローカルキー
            'id' // carsテーブルのローカルキー
        );
    }
}

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;

class Project extends Model
{
    /**
     * プロジェクトのすべてのデプロイメントを取得
     */
    public function deployments()
    {
        return $this->hasManyThrough(Deployment::class, Environment::class);
    }
}

hasManyThroughメソッドへ渡たす、最初の引数はアクセスする最終モデルの名前であり、2番目の引数は中間モデルの名前です。

Deploymentモデルのテーブルはproject_idカラムを含んでいませんが、hasManyThroughリレーションは、$project->deploymentsを介してプロジェクトのデプロイメントへのアクセスを提供します。これらのモデルを取得するために、Eloquentは中間のEnvironmentモデルのテーブルのproject_idカラムを検査します。関連した環境IDを見つけ、それらを使用してDeploymentモデルのテーブルをクエリします。

キーの規約

リレーションのクエリを実行するときは、Eloquent外部キー規約を一般的に使用します。リレーションのキーをカスタマイズする場合は、それらを3番目と4番目の引数としてhasManyThroughメソッドに渡たしてください。3番目の引数は、中間モデルの外部キーの名前です。4番目の引数は、最終モデルの外部キーの名前です。5番目の引数はローカルキーであり、6番目の引数は中間モデルのローカルキーです。

class Project extends Model
{
    public function deployments()
    {
        return $this->hasManyThrough(
            Deployment::class,
            Environment::class,
            'project_id', // environmentsテーブルの外部キー
            'environment_id', // deploymentsテーブルの外部キー
            'id', // projectsテーブルのローカルキー
            'id' // environmentsテーブルのローカルキー
        );
    }
}

多対多リレーション

多対多の関係は、hasOneおよびhasManyの関係よりも少し複雑です。多対多の関係の一例は、多くの役割を持つユーザーであり、役割はアプリケーション内の他のユーザーと共有している場合です。たとえば、あるユーザーに「作成者(Author)」と「編集者(Editor)」の役割を割り当てることができます。ただし、これらの役割は他のユーザーにも割り当てる場合があります。したがって、あるユーザーには多くの役割があり、ある役割には多くのユーザーがいます。

テーブル構造

この関係を定義するには、usersroles、およびrole_userの3つのデータベーステーブルが必要です。role_userテーブルは、関連モデル名のアルファベット順を由来としており、user_idカラムとrole_idカラムを含みます。このテーブルは、ユーザーと役割をリンクする中間テーブルとして使用します。

役割は多くのユーザーに属することができるため、単にuser_idカラムをrolesテーブルに配置することはできません。そうすることは、役割が1人のユーザーにのみ属することができることを意味します。複数のユーザーに割り当てられている役割(role)をサポートするには、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;

class User extends Model
{
    /**
     * このユーザーに属する役割
     */
    public function roles()
    {
        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つの関連するモデル名をアルファベット順に結合します。ただし、この規約は自由に上書きできます。その場合、2番目の引数をbelongsToManyメソッドに渡します。

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;

class Role extends Model
{
    /**
     * この役割に属するユーザー
     */
    public function users()
    {
        return $this->belongsToMany(User::class);
    }
}

ご覧のとおり。関係は、App\Models\Userモデルを参照することを除いて、対応する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();

Note: Eloquentが自動で維持するタイムスタンプを利用する中間テーブルには、created_atupdated_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;
}

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

リレーションを定義するときに、wherePivotwherePivotInwherePivotNotInwherePivotBetweenwherePivotNotBetweenwherePivotNullwherePivotNotNullメソッドを使用し、belongsToMany関係クエリによって返される結果をフィルタリングすることもできます。

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

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

多対多の関係の中間(ピボット)テーブルを表すカスタムモデルを定義する場合は、関係定義時にusingメソッドを呼び出してください。カスタムピボットモデルを使用すると、ピボットモデルに追加のメソッドを定義できます。

カスタムの多対多ピボットモデルはIlluminate\Database\Eloquent\Relationships\Pivotクラス、カスタムのポリモーフィック多対多ピボットモデルはIlluminate\Database\Eloquent\Relationships\MorphPivotクラスを拡張する必要があります。たとえば、カスタムのRoleUserピボットモデルを使用するRoleモデルを定義してみましょう。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    /**
     * この役割に属するユーザー
     */
    public function users()
    {
        return $this->belongsToMany(User::class)->using(RoleUser::class);
    }
}

RoleUserモデルを定義するときは、Illuminate\Database\Eloquent\Relationships\Pivotクラスを拡張する必要があります。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Relations\Pivot;

class RoleUser extends Pivot
{
    //
}

Note: ピボットモデルはSoftDeletesトレイトを使用できません。ピボットレコードをソフト削除する必要がある場合は、ピボットモデルを実際のEloquentモデルに変換することを検討してください。

カスタムピボットモデルと増分ID

カスタムピボットモデルを使用する多対多の関係を定義し、そのピボットモデルに自動増分の主キーがある場合は、カスタムピボットモデルクラスでincrementingプロパティを確実にtrueに設定指定してください。

/**
 * IDの自動増分を指定する
 *
 * @var bool
 */
public $incrementing = true;

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

ポリモーフィックリレーションにより、子モデルは単一の関連を使用して複数タイプのモデルに属せます。たとえば、ユーザーがブログの投稿やビデオを共有できるようにするアプリケーションを構築しているとします。このようなアプリケーションで、CommentモデルはPostモデルとVideoモデルの両方に属する可能性があります。

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

テーブル構造

1対1のポリモーフィックリレーションは、一般的な1対1の関係に似ています。ただし、子モデルは単一の関連付けを使用して複数タイプのモデルへ所属できます。たとえば、ブログのPostUserは、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カラムは、imageableリレーションへのアクセス時に返す親モデルの「タイプ」を決めるため、Eloquentが使用します。この場合、カラムにはApp\Models\PostApp\Models\Userのどちらかが入ります。

モデル構造

次に、この関係を構築するために必要なモデルの定義を見てみましょう。

<?php

namespace App\Models;

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(Image::class, 'imageable');
    }
}

class User extends Model
{
    /**
     * ユーザーの画像を取得
     */
    public function image()
    {
        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()
{
    return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');
}

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\Models;

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(Comment::class, 'commentable');
    }
}

class Video extends Model
{
    /**
     * このビデオの全コメント取得
     */
    public function comments()
    {
        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インスタンスのいずれかを返します。

One Of Many(ポリモーフィック)

あるモデルが多くの関連モデルを持つことがありますが、そのリレーションの「最新」または「最も古い」関連モデルを簡単に取得したい場合があります。例えば、Userモデルは多くのImageモデルと関連しており、ユーザーがアップロードした最新の画像を操作する便利な方法を定義したいとします。このような場合には、morphOneというリレーションタイプとofManyメソッドを組み合わせることで実現できます。

/**
 * 最新のイメージを取得
 */
public function latestImage()
{
    return $this->morphOne(Image::class, 'imageable')->latestOfMany();
}

同様に、「最も古い」、または最初のリレーションの関連モデルを取得する方法を定義することができます。

/**
 * ユーザーの最も古い画像を取得
 */
public function oldestImage()
{
    return $this->morphOne(Image::class, 'imageable')->oldestOfMany();
}

latestOfManyoldestOfManyメソッドはデフォルトで、モデルのソート可能な主キーに基づいて、最新または最も古い関連モデルを取得します。しかし、別のソート基準を使って、より大きなリレーションから単一のモデルを取得したい場合もあるでしょう。

例えば、ofManyメソッドを使って、ユーザーが最も"Like"した画像を取得できます。ofManyメソッドは、ソート可能なカラムを第一引数に取り、関連するモデルを検索する際にどの集約関数(minまたはmax)を適用するかを指定します。

/**
 * ユーザーの最も人気のある画像を取得
 */
public function bestImage()
{
    return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');
}

Tip!! より高度な「一対多」リレーションを構築することも可能です。詳しくは、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

Tip!! ポリモーフィックな多対多のリレーションへに飛び込む前に、典型的な多対多の関係に関するドキュメントを読むとよいでしょう。

モデル構造

これで、モデルの関係を定義する準備ができました。PostモデルとVideoモデルの両方に、基本のEloquentモデルクラスによって提供されるmorphToManyメソッドを呼び出すtagsメソッドを定義します。

morphToManyメソッドは、関連モデルの名前と「リレーション名」を引数に取ります。中間テーブル名へ割り当てた名前とそれが持つキーに基づき、"taggable"と言う名前のリレーションで参照します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * 投稿のすべてのタグを取得
     */
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

逆の関係の定義

次に、Tagモデルで、親になる可能性があるモデルごとにメソッドを定義する必要があります。したがって、この例ではpostsメソッドとvideosメソッドを定義します。これらのメソッドは両方とも、morphedByManyメソッドの結果を返す必要があります。

morphedByManyメソッドは、関連モデルの名前と「リレーション名」を引数に取ります。中間テーブル名へ付けた名前とそれが持つキーに基づいて、"taggable"と言う名前のリレーションで参照します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
    /**
     * このタグを割り当てているすべての投稿を取得
     */
    public function posts()
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }

    /**
     * このタグを割り当てているすべての動画を取得
     */
    public function videos()
    {
        return $this->morphedByMany(Video::class, 'taggable');
    }
}

リレーションの取得

データベーステーブルとモデルを定義したら、モデルを介してリレーションへアクセスできます。たとえば、ある投稿のすべてのタグにアクセスするには、tags動的リレーションプロパティを使用します。

use App\Models\Post;

$post = Post::find(1);

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

morphedByManyを呼び出し実行するメソッドの名前へアクセスすることで、ポリモーフィックな子モデルからポリモーフィックリレーションの親を取得できます。今回の場合、Tagモデルのpostsvideosメソッドです。

use App\Models\Tag;

$tag = Tag::find(1);

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

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

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

デフォルトでLaravelは、完全修飾クラス名を使用して関連モデルの"type"を格納します。たとえば、CommentモデルがPostまたはVideoモデルに属する可能性がある前記の1対多関係の例では、デフォルトのcommentable_typeApp\Models\PostApp\Models\Videoのいずれかになります。ただし、これらの値をアプリケーションの内部構造から切り離したい場合も起きるでしょう。

たとえば、モデル名を"type"として使用する代わりに、postvideoなどの単純な文字列を使用したい場合もあります。これにより、モデル名が変更されても、データベース内のポリモーフィックな「タイプ」カラムの値は有効なままになります。

use Illuminate\Database\Eloquent\Relations\Relation;

Relation::enforceMorphMap([
    'post' => 'App\Models\Post',
    'video' => 'App\Models\Video',
]);

You may call the enforceMorphMap method in the boot method of your App\Providers\AppServiceProvider class or create a separate service provider if you wish.

モデルのgetMorphClassメソッドを使用して、実行時に指定したモデルのポリモーフィックのエイリアスを取得できます。逆に、Relation::getMorphedModelメソッドを使用して、ポリモーフィックのエイリアスへ関連付けた完全修飾クラス名を取得もできます。

use Illuminate\Database\Eloquent\Relations\Relation;

$alias = $post->getMorphClass();

$class = Relation::getMorphedModel($alias);

Note: 既存のアプリケーションに「ポリモーフィックのマップ」を適用する場合、ポリモーフィックリレーションで使用していたそれまでの、完全修飾クラスを含むデータベース内の*_typeカラム値はすべて、「マップ」名に変換する必要が起きます。

動的リレーション

resolveRelationUsingメソッドを使用して、実行時にEloquentモデル間のリレーションを定義できます。通常のアプリケーション開発には推奨しませんが、Laravelパッケージの開発時には役立つでしょう。

resolveRelationUsingメソッドは、最初の引数に付けたいリレーション名を引数に取ります。メソッドの2番目の引数は、モデルインスタンスを引数に取り、有効なEloquentリレーションの定義を返すクロージャです。通常、サービスプロバイダのbootメソッド内で動的リレーションを設定する必要があります。

use App\Models\Order;
use App\Models\Customer;

Order::resolveRelationUsing('customer', function ($orderModel) {
    return $orderModel->belongsTo(Customer::class, 'customer_id');
});

Note: 動的リレーションを定義するときは、常に明示的にキー名引数をEloquentリレーションメソッドの引数に渡してください。

リレーションのクエリ

すべてのEloquentリレーションはメソッドを使い定義するので、関連モデルをロードするクエリを実際に実行しなくても、リレーションのインスタンスを取得するための、こうしたメソッドを呼び出しできます。さらに、すべてのタイプのEloquentリレーションは、クエリビルダとしても機能するため、データベースに対してSQLクエリを最終的に実行する前に、リレーションクエリに制約を連続してチェーンできます。

たとえば、Userモデルに多くのPostモデルが関連付けられているブログアプリケーションを想像してみてください。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * ユーザーのすべての投稿を取得
     */
    public function posts()
    {
        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リレーションクエリへ制約を追加する必要がない場合は、プロパティであるかのようにリレーションにアクセスできます。たとえば、UserPostのサンプルモデルを引き続き使用すると、次のようにユーザーのすべての投稿にアクセスできます。

use App\Models\User;

$user = User::find(1);

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

動的リレーションプロパティは「遅延読み込み」を実行します。つまり、実際にアクセスしたときにのみリレーションデータが読み込まれます。このため、開発者はEagerロードを使用して、モデルのロード後にアクセスすることがわかっているリレーションを事前ロードすることがよくあります。Eagerロードにより、モデルのリレーションを読み込むために実行する必要のある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;

// code%と似ている単語を含むコメントが少なくとも1つある投稿を取得
$posts = Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
})->get();

// code%と似ている単語を含むコメントが10件以上ある投稿を取得
$posts = Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
}, '>=', 10)->get();

Note: Eloquentは現在、データベース間をまたぐリレーションの存在のクエリをサポートしていません。リレーションは同じデータベース内に存在する必要があります。

インライン関係存在クエリ

リレーションのクエリに付加する単純な1つの条件で、リレーションの存在をクエリしたい場合は、whereRelationwhereMorphRelationメソッドを使用するのが便利です。例として、承認されていないコメントを持つすべての投稿を照会してみましょう。

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メソッドに渡したクロージャは、2番目の引数として$type値を受け取ります。この引数を使用すると、作成中のクエリの「タイプ」を調べることができます。

use Illuminate\Database\Eloquent\Builder;

$comments = Comment::whereHasMorph(
    'commentable',
    [Post::class, Video::class],
    function (Builder $query, $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メソッドは結果のモデル上へ{リレーション}_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 ($query) {
    $query->where('rating', 5);
}])

リレーションのカウントとカスタムSELECT文

withCountselectステートメントと組み合わせる場合は、selectメソッドの後にwithCountを呼び出してください。

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

その他の集計関数

Eloquentは、withCountメソッドに加えて、withMinwithMaxwithAvgwithSumwithExistsメソッドも提供しています。これらのメソッドは、結果のモデルに{リレーション}_{集計機能}_{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"リレーションとそのリレーションが返す可能性のあるさまざまなエンティティの関連モデル数をEagerロードしたい場合は、withメソッドをmorphToリレーションのmorphWithCountメソッドと組み合わせて使用​​します。

今回の例では、PhotoモデルとPostモデルがActivityFeedモデルを作成していると想定します。ActivityFeedモデルは、特定のActivityFeedインスタンスの親PhotoまたはPostモデルを取得できるようにするparentableという名前の"morph to"リレーションを定義すると想定します。さらに、Photoモデルには「多くの(have many)」Tagモデルがあり、Postモデルも「多くの(have many)」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'],
]);

Eagerロード

プロパティとしてEloquentリレーションへアクセスすると、関連するモデルは「遅延読み込み」されます。つまりこれは、最初にプロパティへアクセスするまで、リレーションデータが実際にロードされないことを意味します。ただし、Eloquentは、親モデルにクエリを実行するときに、関係を「Eager(積極的)ロード」できます。Eagerロードにより、「N+1」クエリの問題が軽減されます。N+1クエリの問題を説明するために、Authorモデルに「属する(belongs to)」Bookモデルについて考えてみます。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    /**
     * その本を書いた著者を取得
     */
    public function author()
    {
        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回の追加クエリです。

ありがたいことに、Eagerロードを使用し、この操作を2つのクエリに減らすことができます。クエリを作成するときに、withメソッドを使用してどの関係をEagerロードするかを指定します。

$books = Book::with('author')->get();

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

この操作では、2クエリのみ実行します。1回はすべての本を取得するクエリで、もう1回はすべての本のすべての著者を取得するクエリです。

select * from books

select * from authors where id in (1, 2, 3, 4, 5, ...)

複数リレーションのEagerロード

状況により、いくつか異なるリレーションをEagerロードする必要がおきます。これには、関係の配列をwithメソッドに渡すだけです。

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

ネストしたEagerロード

リレーションのリレーションをEagerロードするために、「ドット」構文が使えます。たとえば、本のすべての著者とすべての著者の個人的な連絡先をEagerロードしましょう。

$books = 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();
    }
}

この例では、EventPhoto、およびPostモデルが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 = Book::with('author:id,name,book_id')->get();

Note: この機能を使用するときは、取得するカラムのリストで常にidカラムと関連する外部キーカラムを含める必要があります。

デフォルトのEagerロード

モデルを取得するときに、常にいくつかのリレーションをロードしたい場合があります。実現するには、モデルに$withプロパティを定義します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

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

    /**
     * 本のジャンルを取得
     */
    public function genre()
    {
        return $this->belongsTo(Genre::class);
    }
}

一度のクエリで$withプロパティからのアイテムを削除する場合は、withoutメソッドを使用します。

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

一度のクエリに対し、$withプロパティ内のすべてのアイテムをオーバーライドしたい場合は、withOnlyメソッドが使えます。

$books = Book::withOnly('genre')->get();

Eagerロードの制約

リレーションをEagerロードするだけでなく、Eagerロードクエリへクエリ条件を追加指定したい場合もあります。そのためには、リレーションの配列をwithメソッドへ渡します。ここでの配列キーはリレーション名であり、配列値はEagerロードクエリへ制約を追加するクロージャです。

use App\Models\User;

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

この例では、Eloquentは、投稿のtitleカラムにcodeという単語を含んでいる投稿のみをEagerロードします。他のクエリビルダメソッドを呼び出して、Eagerロード操作をさらにカスタマイズすることもできます。

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

Note: Eagerロードを制限する場合は、limitおよびtakeクエリビルダメソッドは使用できません。

morphToリレーションのEagerロードの制約

morphToリレーションをEagerロードする場合、Eloquentは複数のクエリを実行して各タイプの関連モデルをフェッチします。MorphToリレーションのconstrainメソッドを使用して、これらの各クエリに制約を追加できます。

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\MorphTo;

$comments = Comment::with(['commentable' => function (MorphTo $morphTo) {
    $morphTo->constrain([
        Post::class => function (Builder $query) {
            $query->whereNull('hidden_at');
        },
        Video::class => function (Builder $query) {
            $query->where('type', 'educational');
        },
    ]);
}])->get();

この例でEloquentは、非表示にされていない投稿とtype値が"educational"な動画のみをEagerロードします。

遅延Eagerロード

親モデルを取得した後に、リレーションをEagerロードしたい場合があります。たとえば、これは関連モデルをロードするかを動的に決定する必要がある場合で役立ちます。

use App\Models\Book;

$books = Book::all();

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

Eagerロードクエリにクエリ制約を追加設定する必要がある場合は、ロードしたいリレーションをキーにした配列を渡します。配列の値は、クエリインスタンスを引数に受けるクロージャインスタンスの必要があります。

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

未ロードの場合にのみリレーションシップを読み込むには、loadMissingメソッドを使用します。

$book->loadMissing('author');

ネストした遅延EagerロードとmorphTo

morphToリレーション、およびそのリレーションが返す可能性のあるさまざまなエンティティのネストした関係をEagerロードしたい場合は、loadMorphメソッドを使用できます。

このメソッドは、最初の引数として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モデルインスタンスを取得し、すべてのparentableモデルとそれぞれのネストしたリレーションをEagerロードしてみます。

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

遅延ロードの防止

前述したように、リレーションのEagerロードは、しばしばアプリケーションのパフォーマンスに大きなメリットをもたらします。そのため、ご希望であれば、Laravelにリレーションの遅延ロードを常に防ぐように指示できます。そのためには、Eloquentの基本モデルクラスが提供しているpreventLazyLoadingメソッドを呼び出します。一般的には、アプリケーションの AppServiceProvider クラスの boot メソッド内でこのメソッドを呼び出します。

preventLazyLoading`メソッドは、遅延ロードを防止するかを示すオプションの論理値の引数を取ります。例として、運用環境以外では遅延ロードを無効にして、運用コードに遅延ロードするリレーションが誤って存在していても、運用環境では正常に機能し続けるようにしてみましょう。

use Illuminate\Database\Eloquent\Model;

/**
 * アプリケーションの全サービスの初期起動処理
 *
 * @return void
 */
public function boot()
{
    Model::preventLazyLoading(! $this->app->isProduction());
}

遅延ロードを停止したあと、アプリケーションが任意のEloquentリレーションで遅延ロードしようとすると、EloquentはIlluminate\Database\LazyLoadingViolationException例外を投げます。

遅延ロード違反の動作は,handleLazyLoadingViolationsUsingメソッドを使ってカスタマイズできます。例えば、このメソッドを使って、アプリケーションの実行を例外で中断する代わりに、遅延ロード違反をログに記録するだけにするよう指示できます。

Model::handleLazyLoadingViolationUsing(function ($model, $relation) {
    $class = get_class($model);

    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メソッドは、適切なpost_id値を新しいCommentモデルへ自動的に追加します。

複数の関連モデルを保存する必要がある場合は、saveManyメソッドを使用します。

$post = Post::find(1);

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

saveメソッドとsaveManyメソッドは、指定したモデルインスタンスを保存しますが、親モデルへすでにロードしているメモリ内のリレーションには新しいモデルを追加保存しません。savesaveManyメソッドを使用した後にリレーションへアクセスしようと考えている場合は、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();

createメソッド

saveメソッドとsaveManyメソッドに加え、属性の配列を受け取り、モデルを作成してデータベースに挿入するcreateメソッドも使用できます。savecreateの違いは、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.'],
]);

findOrNewfirstOrNewfirstOrCreateupdateOrCreateメソッドを使用して関係のモデルを作成および更新することもできます。

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

多対多リレーション

関連付け/関連解除

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

使いやすいように、attachdetachはIDの配列も引数に取れます。

$user = User::find(1);

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

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

関連の同期

syncメソッドを使用して、多対多の関連付けを構築することもできます。syncメソッドは、中間テーブルに配置するIDの配列を引数に取ります。指定した配列にないIDは、中間テーブルから削除されます。したがってこの操作が完了すると、指定した配列のIDのみが中間テーブルに残ります。

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

IDを使用して追加の中間テーブル値を渡すこともできます。

$user->roles()->sync([1 => ['expires' => true], 2, 3]);

同期したモデルIDごとに同じ中間テーブルの値を挿入したい場合は、syncWithPivotValuesメソッドを使用できます。

$user->roles()->syncWithPivotValues([1, 2, 3], ['active' => true]);

指定した配列から欠落している既存のIDを切り離したくない場合は、syncWithoutDetachingメソッドを使用します。

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

関連の切り替え

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

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

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

リレーションの中間テーブルの既存のカラムを更新する必要がある場合は、updateExistingPivotメソッドを使用します。このメソッドは、更新する中間レコードの外部キーと属性の配列を引数に取ります。

$user = User::find(1);

$user->roles()->updateExistingPivot($roleId, [
    'active' => false,
]);

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

Postに属するComment など、モデルが別のモデルとのbelongsToまたはbelongsToManyの関係を定義している場合、子モデルのが更新時に親のタイムスタンプも更新できると役立つ場合があります。

たとえば、Commentモデルが更新されたときに、所有しているPostupdated_atタイムスタンプを自動的に「更新」して、現在の日時を設定したい場合があるでしょう。これを行うには、子モデルの更新時にupdated_atタイムスタンプを更新する必要があるリレーションの名前を含むtouchesプロパティを子モデルに追加します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * 更新日時を更新すべき全リレーション
     *
     * @var array
     */
    protected $touches = ['post'];

    /**
     * コメントが属する投稿の取得
     */
    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

Note: 親モデルのタイムスタンプは、Eloquentの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)へ移動

その他

?

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