MySQLやPostgres, Oracleなどのリレーショナルデータベースを利用してアプリケーションの構築を行うとシンプルなテーブル構成でもone to oneリレーションシップやone to manyリレーションシップを利用する機会があるのでそれらの機能/設定を理解できるている人は多いかと思います。しかし本文書で説明を行うpolymorphoicリレーションについては利用方法がわかならいまたは利用した経験がないという人が大半だと思います。Laravelのドキュメントに記載されている例を使って、one to oneのpolymorphic リレーションをできるだけたくさんの人に理解してもらえるように説明を行っていきます。

Laravelのバージョンが更新することで変わる機能ではありません。どのバージョンでも活用できる知識なのでデータベースのリレーションの理解を深める上でも理解して損はありません。Laravel8からモデルファイルの保存先がAppからApp\Modelsに変更になっています。
fukidashi

Polymorphicという難しい単語のため直感的に想像もできないことから何か難しいものではという思い込みがあり理解するための努力に躊躇してしまいそうですが1つのモデルを複数のモデルで共有するという単純な仕組みです。仕組みがわかってしまえばたったこれだけと感じるかもしれません。

Polymorphicリレーションについて

ブログのアプリケーションを構築する例を使って説明していきます。ブログアプリケーションにユーザ情報を保存するUserモデルと記事情報を保存するPostモデルを作成し、各ユーザにはプロファイル画像、各記事にはトップ画像を保存できるとします。

通常であれば、各モデルが持つ画像が1枚であれば、Userモデルにプロファイル画像の情報を保存する列、Postモデルにはトップ画像の情報を保存する列を作成することになります。複数の画像の情報を保存したい場合はUser用のUserImageモデル、Post用のPostImageモデルを別に作成し、one to oneやone to manyリレーションでUserモデルやPostモデルと結びつけます。

Polymorphicでは、画像の情報を保存するImageモデルを1つ追加し、UserとPostから同時にリレーションをもたせます。つまり、ImageモデルをUserモデルとPostモデルで共有します。この共有化を行うことがPolymorphicリレーションを持たせることを意味します。

Imageモデルを共有
Imageモデルを共有

Polymorphicのテーブル構造について

Laravelではpolymorphicを使用するためにimageable_idとimageable_typeという2つの列を追加し、この2つの列を使って共有化を実現します。画像情報を保存するのでimageableという名前を付けています。状況に合わせて名前は変更してください。imageable_typeには、App\User(App\Models\User)かApp\Post(App\Models\Post)のどちらかのモデル情報を保存し、imageable_idにはUserかPostのどちらかのidを保存します。

idはUserとPostで同じ値を持つことがありますが、type列でUserかPostを識別できるため、idとtypeを組み合わせることで同じidとtypeを持った行が絶対に重複することはありません。
fukidashi

最終的にテーブルにデータを登録すると以下のような関連を持たせることができます。

テーブルの構造と関係
テーブルの構造と関係

例えばUserテーブルのユーザidが2のユーザのプロファイル画像を登録したい場合は、imageable_typeにはApp\User、imageable_idに2を指定して保存を行います。保存した画像は、imageable_typeにApp\User、imageable_idに2を指定すれば取得することができます。

Polymorphicテーブルを作成

ここまでの説明でてきた2つの画像で2つのモデルで1つのモデルを共有するというpolymorphicのイメージはつかめてもらえたかと思います。ここからは上記の説明の例を元に実際にテーブルを作成して、動作確認を行っていきます。

Userテーブルの作成

usersテーブルのマイグレーションファイルはインストール時にLaravelで作成されますが、構造をシンプルにするためにnameとemailのみの列として作成します。


public function up()
{
Schema::create('users', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('name');
    $table->string('email')->unique();
    $table->timestamps();
});
}

Postテーブルの作成

Post.phpファイルはphp arisan make:modelコマンドで作成します。以下のコマンドでは、migrationファイルも同時に作成されます。


env_test $ php artisan make:model Post -m

postsテーブルにはシンプルにするためname列だけで作成します。


public function up()
{
Schema::create('posts', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('name');
    $table->timestamps();
});
}

Polymorphicテーブルの作成(Image)

つぎにPolymorphicで最も重要なImage.phpファイルを作成します。


env_test $ php artisan make:model Image -m

マイグレーションファイルにはファイルのパスを保存する列とPolymorphicで使用するimageable列には、stringやintegerといった通常の型ではなくmorphs使用して作成します。


public function up()
{
    Schema::create('images', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->string('file_path');
        $table->morphs('imageable');
        $table->timestamps();
    });
}

列の型をmorphsにすることによってどのような列が追加されるか確認を行います。使用するデータベースによってテーブル情報の確認方法は異なりますが、本環境はsqliteを使用しているので、sqliteで確認すると下記のようにimageable_typeとimageable_idが追加されていることがわかります。


sqlite> PRAGMA table_info('images');
0|id|integer|1||1
1|imageable_type|varchar|1||0
2|imageable_id|integer|1||0
3|file_path|varchar|1||0
4|created_at|datetime|0||0
5|updated_at|datetime|0||0
migrationファイルの中でmorphsで列を作成するとLaravelが自動で列名_typeと列名_idの持った2つの列を作成してくれます。
fukidashi

Modelファイルへの追加

UserモデルとImageモデル, PostモデルとImageモデルとの関係をModelファイルを使用して記述します。

User.phpにmorphOneメソッドを追加します。one to oneリレーションシップではhasOneメソッドとなりますが、PolymorphicではmorphOneになります。


public function image()
{
    return $this->morphOne('App\Image', 'imageable');
}
one to manyのpolymorphicではmorphManyを使用します。
fukidashi

Post.phpにも同様にmorphOneメソッドを追加します。


public function image()
{
    return $this->morphOne('App\Image', 'imageable');
}

Imageモデルには、morphToメソッドを追加します。one to oneリレーションシップでのbelongsToメソッドに相当します。


public function imageable()
{
    return $this->morphTo();
}
one to manyのpolymorphicでもmorphToを使用します。
fukidashi

データの作成

Postへのデータ登録

php artisan tinkerを使用して、Imageモデルへのデータの登録を行います。

  1. Postモデルを使用してpostsテーブルに1件データを作成します。作成したデータは$postに保存します。
  2. 次にImageモデルを使用して$imageを作成し、$imeageのfile_pathにimage/post_1.imgを設定します。
  3. Postモデルはimageメソッドを持っているのでimageメソッドを使用して$imageを保存しています。この処理により、$postと$imageが関連づけられます。

保存後のimageテーブルのimageable_typeには、App\Postが入っていることが確認できます。


$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.1.23 — cli) by Justin Hileman
>>> $post = new App\Post();
=> App\Post {#2958}
>>> $post->name = 'first post'
=> "first post"
>>> $post->save();
=> true
>>> $image = new App\Image();
=> App\Image {#2964}
>>> $image->file_path = 'image/post_1.img'
=> "image/post_1.img"
>>> $post->image()->save($image);
=> App\Image {#2964
     file_path: "image/post_1.img",
     imageable_id: 1,
     imageable_type: "App\Post",
     updated_at: "2019-06-25 01:10:19",
     created_at: "2019-06-25 01:10:19",
     id: 1,
   }

データ登録後は、Postに関連する画像は下記のように取得することができます。


>>> App\Post::find(1)->image
=> App\Image {#2971
     id: "1",
     imageable_type: "App\Post",
     imageable_id: "1",
     file_path: "image/post_1.img",
     created_at: "2019-06-25 01:10:19",
     updated_at: "2019-06-25 01:10:19",
   }
>>> App\Post::find(1)->image->file_path
=> "image/post_1.img"

Userへのデータ登録

userへのデータ登録もpostと同様に行うことができます。


$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.1.23 — cli) by Justin Hileman
>>> $user = new App\User()
=> App\User {#2957}
>>> $user->name = 'john'
=> "john"
>>> $user->email = 'john@example.com'
=> "john@example.com"
>>> $user->save()
=> true
>>> $image = new App\Image()
=> App\Image {#2961}
>>> $image->file_path = 'image/profile_A.png'
=> "image/profile_A.png"
>>> $user->image()->save($image)
=> App\Image {#2961
     file_path: "image/profile_A.png",
     imageable_id: 1,
     imageable_type: "App\User",
     updated_at: "2019-06-25 01:26:16",
     created_at: "2019-06-25 01:26:16",
     id: 2,
   }

先程はpostを使用してimageの情報を取得しましたが、今度はimageからuserもしくはpostの情報を取得します。

Imageのidが2にはUserの情報、Imageのidが1にはPostの情報が入っているので結果は下記のようになります。


>>> App\Image::find(2)->imageable
=> App\User {#2966
     id: "1",
     name: "john",
     email: "john@example.com",
     created_at: "2019-06-25 01:25:38",
     updated_at: "2019-06-25 01:25:38",
   }
>>> App\Image::find(1)->imageable
=> App\Post {#2951
     id: "1",
     name: "first post",
     created_at: "2019-06-25 01:06:13",
     updated_at: "2019-06-25 01:06:13",
   }
>>> 

名前がpolymorphicと発音も意味もわからない難しいイメージのある単語なので名前の通り難しいそうという印象を受けてしまいそうですが、シンプルな例を使って動作確認を行うと難しいものではないとわかっていただけたのではないでしょうか。何度も繰り返していますが要は1つのモデルを2つのモデルで共有するための仕組みです。

Polymorphicのクエリー

Polymorphicリレーションに対してクエリーを実行したい場合はwhereHasMorphメソッドを利用して行うことができます。


Image::whereHasMorph('imageable', [User::class, Post::class], function ($query, $type) {
    if ($type === User::class) {
        //Userに関するquery
        // $query-> 
    }

    if ($type === Post::class) {
        //Postに関するquery
        // $query->
    }
})->where(..)->get();