💎

「ポリモーフィック関連」について個人的まとめ

2023/10/29に公開

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

「ArticleとRecipeというmodelがあり、それぞれにコメントを持ちたい」みたいな時に使える関連付けの方法の1つ。(コメント以外だと、いいねとかタグとかにも使えそう)

ポリモーフィック関連を使わない場合の実装

上記をポリモーフィック関連を使わずに実装する場合、以下の2つの方法が考えられる。

1. コメント用のmodelを複数作る

class ArticleComment < ApplicationRecord
  belongs_to :article
end

class Article < ApplicationRecord
  has_many :article_comments
end

class RecipeComment < ApplicationRecord
  belongs_to :recipe
end

class Recipe < ApplicationRecord
  has_many :recipe_comments
end

Article用のコメントと、Recipe用のコメントで、持ちたいデータなどが大きく異なる場合には、この実装方法が良さそう。
ただ、ほとんど同じデータを持つ場合には、同じような処理を複数書くことになるので、DRYじゃない。

2. コメント用のmodelを1つだけ作り、複数の外部キーを持たせる

class Comment < ApplicationRecord
  belongs_to :article, optional: true
  belongs_to :recipe, optional: true
end

class Article < ApplicationRecord
  has_many :comments
end

class Recipe < ApplicationRecord
  has_many :comments
end
commentsテーブルのマイグレーションファイル
create_table :comments do |t|
  t.text :body
  t.references :article, foreign_key: true, index: true, null: true
  t.references :recipe, foreign_key: true, index: true, null: true
  t.timestamps
end

この方法だと、コメント可能なmodelを新たに追加するたびに、commentsテーブルに新しい外部キーのカラムを追加する必要がある。

ポリモーフィック関連を使う場合の実装

class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
end

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

class Recipe < ApplicationRecord
  has_many :comments, as: :commentable
end
commentsテーブルのマイグレーションファイル
create_table :comments do |t|
  t.text :body
  t.integer :commentable_id # article.id や recipe.idが入る
  t.string :commentable_type # "Article"や"Recipe"が入る
  t.timestamps
end

メリット

スキーマやコードがシンプルになる

commentable_idcommentable_typeの2つのカラムだけで多数の異なるmodelとの関連付けができる。
コメント可能なmodelを新たに追加するたびに外部キーのカラムを追加する必要がない。
同様にbelongs_toも1つだけで済み、modelが追加されるたびに新たに書き足す必要がない。

デメリット

発行されるSQLが複雑になる

例えば以下のようなコードを書いた時

article = Article.find(1)
comments = article.comments

普通なら以下のSQLが発行される。

SELECT * FROM comments WHERE article_id = 1;

でもポリモーフィック関連を使っている場合は、以下のようなSQLになる。

SELECT * FROM comments WHERE commentable_type = 'Article' AND commentable_id = 1;

これくらいシンプルな処理ならそれほど気にならないけど、JOINとかがたくさん絡んできたりすると結構複雑になったりする。

eager_loadメソッド、includesメソッドの利用に注意が必要になる

ポリモーフィック関連を使った状態で、eager_loadメソッドを使って以下のようなコードを書くと

Comment.eager_load(:commentable).all

以下のエラーが発生する。

ActiveRecord::EagerLoadPolymorphicError (Cannot eagerly load the polymorphic association :commentable) 

またincludesメソッドは「eager_loadメソッドとpreloadメソッドをよしなに使い分ける」的な動きをするので、includesメソッドでもエラーが発生することがある。

エラーを出さずにassociationを取得したい場合には、以下のように関連のタイプを指定するか

Comment.where(commentable_type: "Article").eager_load(:commentable).all

preloadメソッドを使う方法がある。

Comment.preload(:commentable).all

データベースの外部キー制約が使えない

データベースの外部キー制約は、あるテーブルのカラムが、別のテーブルの特定のカラムの値を参照することを保証するもの。

ポリモーフィック関連を使った場合、(上記の例でいうと)commentable_idがarticles.idと一致することもあれば、recipes.idに一致することもあるため、外部キー制約が使えない。

これが理由で、ポリモーフィック関連はSQLアンチパターンと言われることもある。

その他もろもろ

  • polymorphic = 「多形性の、多型の」 という意味
    • ポリゴンのpolyですね
  • ダックタイピングする(複数のクラスのインターフェースを統一し、同じように扱えるようにする)ためにポリモーフィックを使うこともあるっぽいけど、Rubyだと「関連する各modelに特定のメソッドを実装することを強制する」のがシンドいので、それをメインの目的として使うことは少なそう

Discussion