ポリモーフィック関連付けとは

業務でポリモーフィック関連付けという言葉が出てきたが知らなかったので調べる。

ドキュメント読む

railsguides.jp

ポリモーフィック関連付け(polymorphic association)は、関連付けのやや高度な応用です。ポリモーフィック関連付けを使うと、ある1つのモデルが他の複数のモデルに属していることを、1つの関連付けだけで表現できます。

ポリモーフィックなbelongs_toは、他のあらゆるモデルから利用可能なインターフェイスを設定する宣言と考えてもよいでしょう。

なるほど、分からない。

実際に触ってみる

ドキュメントにあった下記コードを自分の手元でも試してみる。

class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: true
end

class Employee < ApplicationRecord
  has_many :pictures, as: :imageable
end

class Product < ApplicationRecord
  has_many :pictures, as: :imageable
end

まずはそれぞれのモデルを作る。

❯ rails g model employee name:string                                                                                                
❯ rails g model product name:string 
❯ rails g model picture imageable:references{polymorphic} name:string   

生成物。

# app/models
class Employee < ApplicationRecord
end

class Product < ApplicationRecord
end

class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: true
end
# db/migrate
class CreateEmployees < ActiveRecord::Migration[7.0]
  def change
    create_table :employees do |t|
      t.string :name

      t.timestamps
    end
  end
end

class CreateProducts < ActiveRecord::Migration[7.0]
  def change
    create_table :products do |t|
      t.string :name

      t.timestamps
    end
  end
end

class CreatePictures < ActiveRecord::Migration[7.0]
  def change
    create_table :pictures do |t|
      t.references :imageable, polymorphic: true, null: false
      t.string :name

      t.timestamps
    end
  end
end

マイグレーション

❯ rails db:migrate

生成されたスキーマは次の通り。

# db/schema.rb
  create_table "employees", force: :cascade do |t|
    t.string "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "products", force: :cascade do |t|
    t.string "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "pictures", force: :cascade do |t|
    t.string "imageable_type", null: false
    t.integer "imageable_id", null: false
    t.string "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["imageable_type", "imageable_id"], name: "index_pictures_on_imageable"
  end

ドキュメントの通り Employee モデルと Product モデルに has_many 関連付けを書き足す。

# app/models
class Employee < ApplicationRecord
  has_many :pictures, as: :imageable
end

class Product < ApplicationRecord
  has_many :pictures, as: :imageable
end

rails c で実際にオブジェクトを作りながら試していく。

# Employee
> emp = Employee.create
=> #<Employee:0x00000001142d7ef8 id: 2, name: nil, created_at: Fri, 02 Jun 2023 15:12:56.040521000 UTC +00:00, updated_at: Fri, 02 Jun 2023 15:12:56.040521000 UTC +00:00>

> pic1 = Picture.create(imageable: emp)
=> #<Picture:0x0000000111df3600 id: 2, imageable_type: "Employee", imageable_id: 2, name: nil, created_at: Fri, 02 Jun 2023 15:14:20.147889000 UTC +00:00, updated_at: Fri, 02 Jun 2023 15:14:20.147889000 UTC +00:00>

# Product
> prd = Product.create
=> #<Product:0x00000001147f7988 id: 3, name: nil, created_at: Fri, 02 Jun 2023 15:18:33.911347000 UTC +00:00, updated_at: Fri, 02 Jun 2023 15:18:33.911347000 UTC +00:00>

> pic2 = Picture.create(imageable: prd)
=> #<Picture:0x0000000115b551b0 id: 4, imageable_type: "Product", imageable_id: 3, name: nil, created_at: Fri, 02 Jun 2023 15:18:37.119137000 UTC +00:00, updated_at: Fri, 02 Jun 2023 15:18:37.119137000 UTC +00:00>

Picture が持つ imageable_id に異種モデル (Employee と Product) の id を紐付いている。なんとも不思議な光景だ。これを実現するのが imageable_type である。pic1 と pic2 を見るとそれぞれのモデル名が入っていることが分かる。これにより異種モデルであっても imageable_id がどちらのモデルのものかを識別できている。

使い所

モデル A を異種モデル B, C に対して紐づけたい。かつ、B, C モデルにおける A の扱いが対称性を持つときに便利。(うまく説明できない) 上のサンプルで Picture の所有者は Employee Product のどちらかになる。それをポリモーフィック関連付けなしで実現すると、

class Picture < ApplicationRecord
  belongs_to : employee
  belongs_to : product
end

class Employee < ApplicationRecord
  has_many :pictures
end

class Product < ApplicationRecord
  has_many :pictures
end

となり、そのテーブル構造は次のようになる。

column
id
name
employee_id
product_id

一見問題はないが、今回のケースで EmployeeProduct が同時に1つの Picture に関連付けられることはないため冗長な構造だと言える。なぜならどちらかのカラムは必ず NULL になるから。ならば共通のインターフェースとして imageable_id を設けてあげる。そして EmployeeProduct からは "同質" なものとして扱いましょう。ということだと理解した。

もっと理解できるよい記事

qiita.com