「ポリモーフィック関連」について個人的まとめ
ポリモーフィック関連とは
「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
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
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_id
とcommentable_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