one to oneやone to manyリレーションについてはわかっているが、polymorphoicリレーションについてはわからないという人も多いと思います。本文書ではLaravelのマニュアル記載の例を使って、one to oneのpolymorphic リレーションをできるだけたくさんの人に理解してもらえるように説明を行っていきます。

Polymorphicリレーションについて

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

通常であれば、各モデルにひもづく画像が1枚であれば、Userモデルにプロファイル画像の情報を保存する列、Postモデルにはトップ画像の情報を保存する列を作成することになります。たとえ画像の情報を別のモデルで作ることになってもone to oneやone to manyリレーションの延長上であれば、User用のUserImageモデル、Post用のPostImageモデルを作ることになるかと思います。

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

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

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

Laravelではpolymorphicを使用するためにimageable_idとimageable_typeという2つの列を追加し、この2つの列を使って共有化を実現します。imageable_typeには、App\UserかApp\Postのどちらかのモデル情報を保存し、imageable_idにはUserかPostのどちらかのidを保存します。

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

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

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

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

Polymorphicテーブルを作成

ここまでの説明で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
morphsで列を作成するとLaravelが自動で列名_typeと列名_idの持った2つの列を作成してくれます。

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を使用します。

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を使用します。

データの作成

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つのモデルで共有するための仕組みです。