🐘

Rails のポリモーフィック関連付けの解説と方法

2024/01/09に公開

概要

現場でポリモーフィック関連付けを使用する機会がありましたので、記事を書きました。
オブジェクト指向のポリモーフィズムと絡めて勉強してみたので、もしも認識の違いなどあればコメントくださいませ。
https://railsguides.jp/association_basics.html#ポリモーフィック関連付け

ポリモーフィックと似ている、ポリモーフィズムについて

オブジェクト指向の三大要素の1つとして、「ポリモーフィズム」が存在します。
ポリモーフィズムとは、「多様性」という意味があり、同一のインターフェースやメソッド名を持つ異なるオブジェクトが、それぞれ異なる振る舞いをすることを言います。

例えば「犬」と「猫」のクラスを実装するとき、それらを抽象化した「動物」のクラスを実装することが多いはずです。

そして、犬と猫にそれぞれ「鳴く」というメソッドを実装するときは、「動物」クラスのメソッドをオーバーライドすることになります。

この「鳴くというメソッドが共通で存在し、クラスの種類(今回の「犬」と「猫」)によってそれぞれ異なる振る舞いをする」状態が、ポリモーフィズムの概念に当てはまると言えます。

class Animal
  def make_sound # これがポリモーフィズムの概念に当てはまる。
    raise "オーバーライドしてね"
  end
end

class Dog < Animal
  def make_sound
    "ワン"
  end
end

class Cat < Animal
  def make_sound
    "ニャー"
  end
end

> Dog.new.make_sound
=> "ワン"

> Cat.new.make_sound
=> "ニャー"

このように、make_sound メソッドは、異なるクラスに対して「多様性」を持つことができるわけです。

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

先程のオブジェクト指向プログラミングのポリモーフィズムに対して、今回の Rails のポリモーフィック関連では、

「同じインターフェース(関連付け名)が共通で存在し、関連付けられているモデルの種類によって、それぞれ異なる振る舞いをする状態」がポリモーフィック関連付けです。

ポリモーフィック関連付けの手順

具体的な実装方法をみていきましょう。

今回は以下のような関連付けを作成していきます(わかりやすさのため、ポリモーフィック関連付けに関係するカラムのみを抽出しています)。

commentable_type でどのモデルに関連しているかを判定できるようにしています。
共通の関連付け名は 〇〇able とするのが一般的なようです。

migration の作成

comments テーブルに必要なカラムをもたせます。

class CreateComments < ActiveRecord::Migration[7.0]
  def change
    create_table :comments do |t|
      t.text :content, null: false
      t.references :commentable, polymorphic: true, null: false

      t.timestamps
    end
  end
end

もしも既に article_id や post_id を持っている場合は、そのカラム名を変更します。

model にアソシエーションを追加

class Article < ApplicationRecord
  has_many :comments, as: :commentable
end

class Post < ApplicationRecord
  has_many :comments, as: :commentable
end

class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true # これが異なるインターフェース間での共通の関連付け名
end

動作確認

動作確認のため、以下のようなデータを作成しておきます。

+----+-------------------+------------------+----------------+----------------------------+----------------------------+
| id | content           | commentable_type | commentable_id | created_at                 | updated_at                 |
+----+-------------------+------------------+----------------+----------------------------+----------------------------+
|  1 | article commnet 1 | Article          |              1 | 2024-01-09 23:19:54.000000 | 2024-01-09 23:19:56.000000 |
|  2 | article comment 2 | Article          |              1 | 2024-01-09 23:22:58.000000 | 2024-01-09 23:23:00.000000 |
|  3 | article comment 3 | hogehoge         |              1 | 2024-01-09 23:24:02.000000 | 2024-01-09 23:24:03.000000 |
|  4 | post comment 1    | Post             |              1 | 2024-01-09 23:30:33.000000 | 2024-01-09 23:30:35.000000 |
|  5 | post comment 2    | Post             |              1 | 2024-01-09 23:31:41.000000 | 2024-01-09 23:31:43.000000 |
+----+-------------------+------------------+----------------+----------------------------+----------------------------+

rails console で、 Article と Post それぞれに紐づいているオブジェクトが取得できていることを確認できました。

> Article.first.comments
=>
[#<Comment:0x0000ffff99c3e328
  id: 1,
  content: "article commnet 1",
  commentable_type: "Article",
  commentable_id: 1,
  created_at: Tue, 09 Jan 2024 23:19:54.000000000 UTC +00:00,
  updated_at: Tue, 09 Jan 2024 23:19:56.000000000 UTC +00:00>,
 #<Comment:0x0000ffff99c3e120
  id: 2,
  content: "article comment 2",
  commentable_type: "Article",
  commentable_id: 1,
  created_at: Tue, 09 Jan 2024 23:22:58.000000000 UTC +00:00,
  updated_at: Tue, 09 Jan 2024 23:23:00.000000000 UTC +00:00>]
  
> Post.first.comments
=>
[#<Comment:0x0000ffff99c9dd50
  id: 4,
  content: "post comment 1",
  commentable_type: "Post",
  commentable_id: 1,
  created_at: Tue, 09 Jan 2024 23:30:33.000000000 UTC +00:00,
  updated_at: Tue, 09 Jan 2024 23:30:35.000000000 UTC +00:00>,
 #<Comment:0x0000ffff99c9db48
  id: 5,
  content: "post comment 2",
  commentable_type: "Post",
  commentable_id: 1,
  created_at: Tue, 09 Jan 2024 23:31:41.000000000 UTC +00:00,
  updated_at: Tue, 09 Jan 2024 23:31:43.000000000 UTC +00:00>]

ちなみに流れている SQL は以下です。
commentable_idcommentable_type で検索しているのがわかりますね。

> Article.first.comments.to_sql
=> "SELECT `comments`.* FROM `comments` WHERE `comments`.`commentable_id` = 1 AND `comments`.`commentable_type` = 'Article'"

どんな時に便利か?

同じようなテーブル構造で、親のテーブルを共通にしたいときに使えます。

以下のようなテーブルの関係よりも、ポリモーフィック関連付けを使ったほうがシンプルですし、
コードの重複も少なくなります。

  • articles テーブルに対応する article_comments テーブル
  • posts テーブルに対応する post_commnents テーブル

Discussion