Help us understand the problem. What is going on with this article?

【初心者向け】Railsのポリモーフィック関連付けを理解しよう

More than 1 year has passed since last update.

はじめに

お仕事でRailsのポリモーフィック関連付けを使う機会があり、非常に便利だなぁと思ったので、今更ながらまとめてみました。

ポリモーフィックとは

ポリモーフィック(ポリモーフィズム)という言葉の意味自体は多様な(多様性)という意味です。
何が多様やねんという感じですね。

オブジェクト指向の世界における意味としては、複数の異なる型に対して共通のインタフェースを提供することです。
つまり、複数の異なるオブジェクトが同じメッセージに応答できることを意味します。

実装ではなくてインターフェースに応答するというので、これはダックタイプの一種です。

ダックタイプの例

例えば calc_rewardという報酬を算出するメソッドがあったとします。
RegularEmployeeクラス(正社員クラス)とContractEmployeeクラス(契約社員クラス)のそれぞれがこのメソッドを備えていれば、具体的な処理内容が違ったとしても、どちらもこのメッセージに対して応答できます。

class RegularEmployee
  def calc_reward
    # 正社員の報酬計算ロジック
  end
end

class ContractEmployee
  def calc_reward
    # 契約社員の報酬計算ロジック
  end
end

# employeesには正社員と契約社員のオブジェクトが配列で入っていると仮定
employees.each(&:calc_reward)
# => 全てオブジェクトはcalc_rewardメソッドに応答できるので、オブジェクトごとに適切な処理を実行する

# よくある残念な例
# ダックタイプを活用できていないと、case文で分岐してそれぞれの処理を記述する羽目になる...
employees.each do |employee|
  case employee
  when regular
    employee.calc_regular_reward
  when contract
    employee.calc_contract_reward
  end
end

ダックタイプの詳細に関しては、別途記事を書いていますのでそちらを参照ください。
【OOP入門】Rubyでダックタイピングを理解する

話を戻して、
ポリモーフィックとはダックタイプの一種であり、複数の異なる型やオブジェクトに対して共通のインターフェースを備えることです。

Rails Guides - Active Record の関連付け (アソシエーション)によると、Railsで提供されているポリモーフィック関連付けに関しては、

ポリモーフィック関連付けを使用すると、ある1つのモデルが他の複数のモデルに属していることを、1つの関連付けだけで表現することができます。

とのこと。

使い方

例としてユーザー(User)と企業(Company)があり、それぞれがプロフィール(Profile)を持ちたくなった場合を想定します。
従来であれば下記のように、企業 - プロフィールユーザー - プロフィールのそれぞれに中間テーブルを作る構成が一般的かなと思います。

なんとなくのイメージ

Screen Shot 2018-12-01 at 9.20.36.png

この場合、関連付けたい要素が増えるほど、中間テーブルを用意する必要が出てきます。
一方でこれに対してポリモーフィック関連付けの考え方を用いると、
プロフィールはユーザーと企業というそれぞれ全く別のモデルに対して属するように考えられます。

なんとなくのイメージ

Screen Shot 2018-12-01 at 9.08.57.png

Railsでポリモーフィック関連付けを行う場合、マイグレーションとモデルでそれぞれ下記のような準備が必要になります。

マイグレーション

子のモデルであるプロフィールを作る際に、polymorphic: truexxxable_idxxxable_typeを設定します。
*慣習的に自身のモデル名をxxxableとするようです

xxxable_idには、親のモデルであるユーザーまたは企業のIDが入ります。
xxxable_typeには、親のモデルであるユーザーまたは企業のクラス名(userやcompany)が入ります。
Railsはこのidとtypeの2つを用いて、そのプロフィールがユーザーであるか企業であるかを判別します。

class CreatePictures < ActiveRecord::Migration[5.0]
  def change
    create_table :pictures do |t|
      t.string  :name
      t.integer :profilable_id
      t.string  :profilable_type
      t.timestamps
    end

    add_index :pictures, [:profilable_type, :profilable_id]
  end
end

# または下記でもok
class CreateProfiles < ActiveRecord::Migration[5.0]
  def change
    create_table :pictures do |t|
      t.string  :name
      t.references :profilable, polymorphic: true, index: true
      t.timestamps
    end
  end
end

モデル

  • 子モデルにbelongs_to :xxxable, polymorphic: trueを設定
    • 慣習的に自身のモデル名をxxxableとするようです
  • 親モデルにhas_many :xxxable, as: :profilableを設定
class Profile < ApplicationRecord
  belongs_to :profilable, polymorphic: true
end

class User < ApplicationRecord
  has_many :profiles, as: :profilable
end

class Company < ApplicationRecord
  has_many :profiles, as: :profilable
end

これにより、プロフィールはユーザー、企業に属するモデルとなりました。
プロフィールから親(ユーザーor企業)を取得する場合にはProfile.first.profilableと記述します。
逆にユーザーや企業からプロフィールを取得する場合には、User.first.profilesと記述します。

このようにアプリケーション側では関連付けを意識することなく、子から親、親から子のモデルを呼び出すことができます。

インターフェースを強制する

Rubyにはインターフェースの定義は必須ではありませんが、ポリモーフィック関連付けをすることで必然的に全てが同じメッセージに応答できる必要が出てきます。

そのため、インターフェースの雛形をモジュールで作り、そのモジュールを関連付け対象のモデル側でincludeすることによってインターフェースを強制すると、「あのモデルでは定義されているけどこのモデルには定義されていないメソッドがある」といった事態を防ぐことができます。

module Profilable
  extend ActiveSupport::Concern

  included do
    has_many :profiles, as: :profilable
  end

  def title
    NotImplementedError
  end

  def body
    raise NotImplementedError
  end
end

class User < ApplicationRecord
  include Profilable
  def title
  end

  def body
  end
end

class Company < ApplicationRecord
  include Profilable
  def title
  end

  def body
  end
end

関連付けられているモデルが共通のインターフェースを持つことで、関連付け先の対象をcase文やif文で場合分けするようなことをしなくて済みます。

# 何かが間違っている...
def title
  case type_id
  when user
    ...
  when company
    ...
  end
end

注意点

SQLアンチパターンにも掲載されているように、ポリモーフィック関連付けはDBにとっては一つのアンチパターンとされています。

ポリモーフィック関連付けを実装すると、主にDBに関して下記の問題が生じます。

  • 一つのカラムで複数テーブルのidを参照しているため、外部キーが設定できない
  • 紐づく親のテーブル情報をxxx_typeというカラムにstring型で格納する必要がある

これらにより、DB側だけではデータの整合性を担保できなかったり、SQLで結合する際には関連する全てのテーブルを結合する必要があります。
アプリケーション側ではORMの力を使って親子間のデータを簡単に取得することができる一方で、その分DB側にしわ寄せがくる感じです。

そのため、採用する際には、それぞれにメリット・デメリットを理解した上で採用する必要があります。

私見としては、開発者できちんとルールを決めて運用する分にはポリモーフィック関連付けをしても良いと思います。
一方で、入れ替わり立ち代わりの激しいチームやコミュニケーションに難のあるチームではむやみに扱うと保守メンテが大変になるかなと思います。

参考

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away