開発者は日々の業務の中でしばしば、データベースとの通信を扱うことになります。そんな時に便利なのが、Laravelのオブジェクト関係(ORM)マッピングであるEloquentです。これを使うことで、データベースのテーブルとのやり取りが直感的かつ自然なものになります。

これを扱う上で、6つのタイプについて理解を深めておくことが欠かせません。こちらの記事では、その種類を詳しくご紹介したいと思います。

Eloquentのリレーションとは

リレーショナルデータベースでテーブルを扱う際には、リレーションシップをテーブル間の接続として定義することができます。これにより、データを簡単に整理・構造化し、優れた可読性とデータの取り扱いが可能になります。データベースのリレーションには、実際には3つのタイプがあります。

  • 1対1─あるテーブルの1つのレコードが、別のテーブルの1つのレコードに関連付けられます。例えばある人物と社会保障番号などです。
  • 1対多─1つのレコードが別のテーブルの複数のレコードに対応付けられます。例えば、ライター(執筆者)と複数のブログ記事です。
  • 多対多─あるテーブルの複数のレコードが、別のテーブルの複数のレコードに関連付けられます。例えば、学生とその受講コースなどです。

Laravelでは、Eloquentのオブジェクト指向構文を使用して、データベースのリレーションとの通信や管理をシームレスに行うことができます。

上記の基本にあわせて、Laravelにはさらなるリレーションが導入されています。

  • 「hasManyThrough」
  • 「Polymorphic Relations」
  • 「Many-to-many Polymorphic」

例えば、在庫に様々な商品があり、それぞれがカテゴリーに分けられているようなお店を考えてみましょう。情報整理の観点から、データベースを複数のテーブルに分割することができます。このようなケースで、一つひとつのテーブルにクエリをかけるのは賢いやり方とは言えません。

Laravelでは、単純な1対多のリレーションを簡単に作成することができます。例えば、商品をクエリする必要がある場合、Productモデルを使うことが可能です。

多相リレーションを表すデータベーススキーマ(3つのテーブルと1つの結合テーブル)
多相リレーションを表すデータベーススキーマ(3つのテーブルと1つの結合テーブル)

1対1のリレーション

Laravelが提供する最初の基本的なリレーションがこちらです。2つのテーブルがあります。1つ目のテーブルの1行がもう1つのテーブルの1行と相関するようになっています。

これが実際に機能するところを確認するために、それぞれ独自のマイグレーションを持つ2つのモデルを作成してみましょう。

php artisan make:model Tenant 
Php artisan make:model Rent

この時点で、2つのモデルが用意できました。ひとつは居住者で、もうひとつは家賃です。

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Tenant extends Model
{
    /**
    * 居住者の家賃を取得
    */
    public function rent() 
    {
        return $this->hasOne(Rent::class);
    }
}

Eloquentは親モデル名(この例ではTenant)に基づいて外部キーリレーションを決定するため、Rentモデルはtenant_id外部キーが存在すると仮定します。

そして必要であれば、これをhasOneメソッドの引数を使うことで簡単に上書きできます。

return $this- >hasOne(Rent::class, "custom_key");

また、Eloquentは定義した外部キーと親(Tenantモデル)の主キーが一致することを前提としています。デフォルトでは、tenant_idとTenantレコードのidキーの一致が探されます。hasOneメソッドの3番目の引数でこれを上書きし、別のキーにマッチさせることが可能です。

return $this->hasOne(Rent::class, "custom_key", "other_key"); 

モデル間の1対1のリレーションを定義したので、以下のように使うことができます。

$rent = Tenant::find(10)->rent;

このコードで、入居者の家賃を取得します。

1対多のリレーション

前のリレーションのように、こちらは単一の親モデルと複数の子モデル間のリレーションを定義するものです。家賃は定期的な支払いであるため、請求情報が1つだけということは恐らくないでしょう。

この場合、以前のリレーションには欠点があります。修正してみましょう。

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Tenant extends Model
{
    /**
    * 居住者の賃料を取得
    */
    public function rent() 
    {
        return $this->hasMany(Rent::class);
    }
}

家賃を取得するメソッドを呼び出す前に知っておくべきこととして、リレーションはクエリビルダーの役割を果たすため、他に制約(特定の期間の家賃、最低支払額など)を追加しそれらを連鎖させることができます。

$rents = Tenant::find(10)->rent()->where('payment', '>', 500)->first();

また、先ほどのリレーション同様、追加の引数を渡すことで外部キーやローカルキーを上書きすることもできます。

return $this->hasMany(Rent::class, "foreign_key");
return $this->hasMany(Rent::class, "foreign_key", "local_key");

これで家賃はすべて把握できました。それでは、家賃が分かっていてそれが誰のものかを知りたいときにはどうすればよいでしょうか。belongsToプロパティを使うことができます。

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Rent extends Model
{
    /**
    * 賃料に対応する居住者を返す
    */
    public function tenant() 
    {
        return $this->belongsTo(Tenant::class);
    }
}

そして簡単に居住者を取得できます。

$tenant = Rent::find(1)->tenant;

belongsToメソッドでは、先ほどと同じように外部キーとローカルキーを上書きすることもできます。

「Has-One-Of-Many」リレーション

Tenantモデルは多くのRentモデルと関連する可能性があり、最新または最も古いモデルを簡単に取得できると便利です。

これを行う便利な方法として、hasOneメソッドとofManyメソッドを組み合わせることができます。

public function latestRent() {
    return $this->hasOne(Rent::class)->latestOfMany();
}

public function oldestRent() {
    return $this->hasOne(Rent::class)->oldestOfMany();
}

デフォルトでは、ソート可能な主キーに基づいてデータを取得していますが、ofManyメソッドで独自のフィルタを作成することもできます。

return $this->hasOne(Rent::class)->ofMany('price', 'min');

「HasOneThrough」と「HasManyThrough」のリレーション

Throughメソッドは、他のモデルを経由して、目的のモデルとのリレーションを確立することを示唆します。例えば、家賃を家主に関連付けることができますが、家賃が家主に到達するためには、まず居住者を経由しなければならないといった具合です。

これに必要なテーブルのキーは次のようになります。

rent
    id - integer
    name - string
    value - double

tenants
    id - integer
    name - string
    rent_id - integer

landlord
    id - integer
    name - string
    tenant_id - integer

テーブルの様相を視覚化した後で、モデルを以下のように作ります。

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Rent extends Model
{
    /**
    * 家賃に対応した家主を返す
    */
    public function rentLandlord() 
    {
        return $this->hasOneThrough(Landlord::class, Tenant::class);
    }
}

hasOneThroughメソッドの第一引数はアクセスしたいモデルで、第二引数は経由するモデルです。

そして先ほどと同じように、外部キーとローカルキーを上書きします。2つのモデルがあるので、それぞれ2つずつ以下の順番で上書きします。

public function rentLandlord() 
{
    return $this->hasOneThrough(
        Landlord::class,
        Tenant::class,
        "rent_id",    // Foreign key on the tenant table
        "tenant_id",  // Foreign key on the landlord table
        "id",         // Local key on the tenant class
        "id"          // Local key on the tenant table
    );
}

同様に、Laravel Eloquentの「Has Many Through」リレーションは、中間テーブルを介して離れたテーブルのレコードにアクセスしたい場合に便利です。以下の3つのテーブルがある例を考えてみましょう。

  • country
  • users
  • games

それぞれの国(Country)にたくさんのユーザー(Users)がいて、それぞれのユーザー(Users)がたくさんのゲームを持っているとします。ある国(Country)に属するすべてのゲームをUserテーブルから取得してみましょう。

次のようにテーブルを定義できます。

country
    id - integer
    name - string

user
    id - integer
    country_id - integer
    name - string

games
    id - integer
    user_id - integer
    title - string

次に、テーブルごとにEloquentモデルを定義します。

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Country extends Model
{
    protected $fillable = ['name'];

    public function users()
    {
        return $this->hasMany(User::class);
    }

    public function games()
    {
        return $this->hasManyThrough(Games::class, User::class);
    }
}
<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $fillable = [article_id, 'name'];

    public function country()
    {
        return $this->belongsTo(Country::class);
    }

    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}
<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Game extends Model
{
    protected $fillable = ['user_id', 'title'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

「Has Many Through」リレーションをCountryとGameの間で(Userモデルを通じて)確立したため、Countryモデルのgames()メソッドを呼び出して、すべてのゲームを取得できるようになりました。

<?php

$country = Country::find(159);
            
// 国のすべてのゲームを取得する
$games = $country->games;

多対多のリレーション

多対多の関係はより複雑です。例えば、複数の役割を持つ従業員です。同じ役割を複数の従業員に割り当てることもできます。これが多対多のリレーションの基本です。

上の例を実装するには、employeesテーブル、rolesテーブル、role_employeesテーブルが必要です。

データベースのテーブル構造は次のようになります。

employees
    id - integer
    name - string

roles 
    id - integer
    name - string

role_employees
    user_id - integer
    role_id - integer

リレーションのテーブル構造を知っていれば、EmployeeモデルをbelongToMany Roleモデルに簡単に定義できます。

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Employee extends Model
{
    public function roles() 
    {
        return $this- >belongsToMany(Role::class);
    }
}

これを定義すると、従業員のすべての役割にアクセスでき、さらにそれをフィルタリングすることもできます。

$employee = Employee::find(1);
$employee->roles->forEach(function($role) { // });

// または

$employee = Employee::find(1)->roles()->orderBy('name')->where('name', 'admin')->get();

他のメソッド同様、belongsToManyメソッドの外部キーやローカルキーを上書きすることができます。

belongsToManyの逆方向のリレーションを定義するのにも同じメソッドを使えますが、今度は子メソッドに親を引数として与えます。

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    public function employees() 
    {
        return $this->belongsToMany(Employee::class);
    }
}

中間テーブルの使用

すでにお気づきかもしれませんが、多対多のリレーションを使用する場合、常に中間テーブルを持つことになっています。この例では、role_employeesテーブルを使用します。

デフォルトでは、ピボットテーブルにid属性のみが含まれます。他の属性が必要な場合は、以下のように指定する必要があります。

return $this->belongsToMany(Employee::class)->withPivot("active", "created_at");

中間テーブルのタイムスタンプについての手動での処理を省くには、次のようにします。

return $this->belongsToMany(Employee::class)->withTimestamps();

1つのコツとして「pivot」という名前は、好みのものに変更することができます。

return $this->belongsToMany(Employee::class)->as('subscription')->withPivot("active", "created_by");

Laravelアプリケーションを最適化し、ステップアップしたい開発者にとって、Eloquentクエリの結果をフィルタリングすることは必須の知識です。

Laravelにはこれを支える素晴らしい機能があり、収集したいデータのフィルタリングに使用できます。データベーストランザクションのような他の機能を使ってデータを塊で取得する代わりに、wherePivotwherePivotInwherePivotNotInwherePivotBetweenwherePivotNotBetweenwherePivotNullwherePivotNotNullのような便利なメソッドを使ってフィルタリング可能です。これらはテーブル間のリレーションを定義するときに使うことができます。

return $this->belongsToMany(Employee::class)->wherePivot('promoted', 1);
return $this->belongsToMany(Employee::class)->wherePivotIn('level', [1, 2]);
return $this->belongsToMany(Employee::class)->wherePivotNotIn('level', [2, 3]);
return $this->belongsToMany(Employee::class)->wherePivotBetween('posted_at', ['2023-01-01 00:00:00', '2023-01-02 00:00:00']);
return $this->belongsToMany(Employee::class)->wherePivotNull('expired_at');
return $this->belongsToMany(Employee::class)->wherePivotNotNull('posted_at');

最後にもうひとつ、中間テーブルの並び順が選べるという素晴らしい機能も注目に値します。

return $this->belongsToMany(Employee::class)
        ->where('promoted', true)
        ->orderByPivot('hired_at', 'desc');

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

「ポリモーフィック」という言葉はギリシャ語に由来し、「多形体」を意味します。1つのモデルが多くの形を取ることができる、つまり複数の関連付けを持つことができるということです。ブログ、動画、投票などのアプリケーションを構築しているとしましょう。ユーザーはこれらのどれに対してもコメントを残すことができます。したがって、Comment(コメント)モデルはBlogs(ブログ)、Videos(動画)、Polls(投票)モデルのいずれにも属する可能性があります。

ポリモーフィックの「1対1」

このタイプのリレーションは、標準的な1対1のものに似ていますが、唯一の違いとして、子モデルが1つのリレーションで複数のタイプのモデルに属することができます。

例えば、Tenant(居住者)とLandlord(大家)のモデルを例にとると、WaterBill(水道代)モデルとの多相関係を共有する可能性があります。

テーブルの構造は以下のようになります。

tenants
    id – integer
    name – string

landlords
    id – integer
    name – string

waterbills
    id – integer
    amount – double
    waterbillable_id
    waterbillable_type

landlordまたはtenantのidにはwaterbillable_idを使用しています。waterbillable_typeには親モデルのクラス名が入ります。typeカラムはEloquentがどの親モデルを返すかを判断するために使用します。

このような関係のモデル定義は以下のようになります。

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class WaterBill extends Model
{
    public function billable()
    {
        return $this->morphTo();
    }
}

class Tenant extends Model
{
    public function waterBill()    
    {
        return $this->morphOne(WaterBill::class, 'billable');
    }
}

class Landlord extends Model
{
    public function waterBill()    
    {
        return $this->morphOne(WaterBill::class, 'billable');
    }
}

これがすべて整えば、家主(Landlord)と借主(Tenant)の両方のモデルからデータにアクセスすることができます。

<?php

$tenant = Tenant::find(1)->waterBill;
$landlord = Landlord::find(1)->waterBill;

ポリモーフィックの「1対多」

これは、通常の1対多のリレーションと似ていますが、唯一の重要な違いとして、子モデルが1つの関連性を使用して、複数のタイプのモデルに属することができます。

Facebookのようなアプリケーションでは、ユーザーは投稿、動画、投票、ライブなどにコメントすることができます。ポリモーフィックな1対多のリレーションを使用すると、1つのコメント(comments)テーブルを使用して、すべてのカテゴリのコメントを保存することができます。テーブル構造は以下のようになります。

posts 
    id – integer
    title – string
    body – text

videos
    id – integer
    title – string
    url – string

polls
    id – integer
    title – string

comments 
    id – integer
    body – text
    commentable_id – integer
    commentable_type – string

commentable_idはレコードのIDで、commentable_typeはクラスのタイプで、Eloquentで何を探すべきかが明らかになっています。モデルの構造としては、ポリモーフィックの一対多と非常によく似ています。

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model 
{
    public function commentable()
    {
        return $this->morphTo();
    }
}

class Poll extends Model
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

class Live extends Model
{
    public function comments()
    {
        return $this->morphMany(Comments::class, 'commentable');
    }
}

これで、Liveのコメントを取得するために、idを指定してfindメソッドを呼び出すだけで、comments iterableクラスにアクセスできます。

<?php

use App\Models\Live;

$live = Live::find(1);

foreach ($live->comments as $comment) { }

// または

Live::find(1)->comments()->each(function($comment) { // });
Live::find(1)->comments()->map(function($comment) { // });
Live::find(1)->comments()->filter(function($comment) { // });

// etc.

そして、コメントが誰のものか知りたい場合には、commentableメソッドを使用できます。

<?php

use App\Models\Comment;

$comment = Comment::find(10);
$commentable = $comment->commentable;

// commentable – type of Post, Video, Poll, Live

ポリモーフィックの「One of Many」

スケールを意識したアプリケーションでは、各モデル間での通信を簡単にする手法が必要になります。例えば、ユーザーの最初の投稿や最後の投稿を取得したいとします。これはmorphOneメソッドとofManyメソッドを組み合わせることで実現できます。

<?php

public function latestPost()
{
    return $this->morphOne(Post::class, 'postable')->latestOfMany();
}

public function oldestPost()
{
    return $this->morphOne(Post::class, 'postable')->oldestOfMany();
}

主キーがソート可能であるという前提のもとで、latestOfManyメソッドとoldestOfManyメソッドが、モデルの主キーに基づいて最も新しいまたは最も古いモデルを取得します。

場合によっては、IDでのソートを避けたいこともあります。投稿の公開日を変更したケースなどです。そんな時には、IDの順ではなく公開日にならって並べ替えを行うのがいいでしょう。

これを行うには、ofManyメソッドに2つのパラメータを渡します。最初のパラメータはフィルタをかけたいキーで、2番目のパラメータはソートの方法です。

<?php

public function latestPublishedPost()
{
    return $this->morphOne(Post::class, "postable")->ofMany("published_at", "max");
}

これを念頭に置いて、より高度な構造を構築することが可能です。例えば、こんなケースがあるとします。投稿の一覧を公開順に生成するよう依頼されました。ここで問題が発生します。同じpublished_atの値を持つ投稿が2つある場合や、特定の投稿の公開が予定されている(つまりまだ公開されていない)場合はどうすべきでしょうか。

これを行うには、フィルタを適用する順番をofManyメソッドに渡します。この方法では、published_atで並び替え、同じ場合はidで並び替えます。次に、ofManyメソッドにクエリ関数を適用して、スケジュール調整中の投稿をすべて除外することができます。

<?php

public function currentPosts()
{
    return $this->hasOne(Post::class)->ofMany([
        'published_at' => 'max',
        'id' => 'max',
    ], function ($query) {
        $query->where('published_at', '<', now());
    });
}

ポリモーフィックの「Many To Many」

ポリモーフィックの多対多は、通常のものより少し複雑です。タグがアプリケーション内の多くのアセットに適用されることが多々あります。例えば、TikTokでは、動画やストーリーにタグをつけることができます。

ポリモーフィックの多対多を使うことで、動画やストーリーに関連するタグを1つのテーブルにまとめることが可能です。

テーブルの構造はシンプルです。

videos
    id – integer
    description – string

stories 
    id – integer
    description – string

taggables 
    tag_id – integer
    taggable_id – integer
    taggable_type – string

テーブルの準備ができたら、モデルを作成してmorphToManyメソッドを使います。このメソッドはモデルクラス名とリレーション名を受け取ります。

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Video extends Model
{
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

そして、これによって、逆の関係を簡単に定義することができます。すべての子モデルに対して、morphedByManyメソッドを呼び出します。

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
    public function stories()
    {
        return $this->morphedByMany(Story::class, 'taggable');
    }

    public function videos()
    {
        return $this->morphedByMany(Video::class, 'taggable');
    } 
}

そして、タグを取得すると、そのタグに関連するすべての動画やストーリーを取得できます。

<?php
use App\Model\Tag;

$tag = Tag::find(10);
$posts = $tag->stories;
$videos = $tag->stories;

Eloquentのスピード最適化

LaravelのEloquent ORMを使用する際には、データベースクエリを最適化する方法を理解し、データをフェッチするのに必要な時間とメモリを最小限に抑えることが重要です。これを行う1つの方法として、アプリケーションにキャッシュを実装することができます。

Laravelでは、RedisMemcachedファイルベースのキャッシュなど、様々なバックエンドをサポートする柔軟なキャッシュシステムが利用可能です。Eloquentのクエリ結果をキャッシュすることで、データベースクエリの回数を減らし、アプリケーションをより高速で価値あるものにできます。

さらに、Laravelのクエリビルダーを使用して、より複雑なクエリを作成し、アプリケーションのパフォーマンスを一層最適化することも可能です。

まとめ

結論として、EloquentのリレーションはLaravelの便利な機能で、開発者はこれを使うことで関連データを簡単に扱うことができます。一対一のリレーションから多対多のリレーションまで、Eloquentにはこれらのリレーションを定義しクエリするためのシンプルかつ直感的な構文が用意されています。

Laravel開発者にとって、Eloquentリレーションのマスターは必須です。開発ワークフローを大幅に強化し、コードをより効率的で読みやすくすることができます。Kinstaでは、Laravelを使いこなすための解説Laravel開発者の給与に関する記事など、様々なリソースをご用意しています。

Laravelアプリケーションのデプロイと管理を容易にするホスティングソリューションもご確認ください。

Coman Cosmin

3年以上の経験を持つテクニカルライター、開発者。Kinstaでの執筆以外には、核物理学施設や大学での研究を支援。技術に精通し、コミュニティで活発に活動しており、常に革新的なソリューションを考案している。