ポリモーフィック関連のつらみ
ラブグラフでインターンをしているりょうさんです!
前回はポリモーフィック関連についてざっくり理解をしました。
今回はポリモーフィック関連について理解した上でつらい部分を紹介していこうと思います!
モデルや実際のコードは前回の記事(上記)を元に話していこうと思います!
以下のテーブルが存在する程で話します!
# db/migrate/xxxx_create_comments.rb
class CreateComments < ActiveRecord::Migration
def change
create_table :comments do |t|
t.text :content
t.references :commentable, polymorphic: true, null: false
t.timestamps
end
end
end
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
class StudentDiary < ApplicationRecord
has_many :comments, as: :commentable
end
class TeacherNotice < ApplicationRecord
has_many :comments, as: :commentable
end
class ClubBlog < ApplicationRecord
has_many :comments, as: :commentable
end
外部キー制約が貼れない
Rails では DB に対して外部キー制約を宣言する時には、以下のように行います。
add_foreign_key :comments, :posts, column: :commentable_id
comments テーブルに対して行っていますが、これは失敗します。
失敗する理由は、「外部キー制約」と「ポリモーフィック関連」の目的が根本的に矛盾しているためです。
add_foreign_key :comments, :posts は、「commentsテーブルのあるカラム(今回はcommentable_id)の値は、必ずpostsテーブルのidカラムに存在しなければならない」という厳格なルールをデータベースに課します。
一方、ポリモーフィック関連における comments テーブルの commentable_id カラムは、 commentable_type カラムの値に応じて、テーブルの参照先が動的に変わります。(参照先が一意ではない)
これがポリモーフィック関連の「外部キー制約が貼れない」というつらい部分でした。
join と eager_load ができない
ポリモーフィック関連付けを使用すると、「コメントとコメント先を1本の SQL でまとめて取得したい」という普通の要望がやりづらくなります。
まずは素直に joins を書いてみると、
# 「コメントと一緒に commentable の title も取りたい」という気持ち
Comment.joins(:commentable).select('comments.*, commentables.title')
この場合、エラーが出ます。
理由としては、commentable はポリモーフィックなので、実際には StudentDiary や ClubBlog など、複数のテーブルが入り得ます。Rails はそれを動的に判断してクエリを分けてくれますが、joins で 1 つのテーブル に決め打ちすることはできません。
もし関連先の情報を取得するなら、データベースを JOIN せずに、クエリを発行する手順を考えます。となると考えられる手段は、includes や preload ですね。個人的には、includes だとなんのクエリが走るのかが曖昧なので、preload の方が好みです。
こうすることにより、関連先が動的な場合でもデータを取得できます。
もしどうしてもJOINしたいケースや、JOINが業務要件として常態化するなら、ポリモーフィックをやめる設計変更が一番きれいになるでしょう。
まとめ
ポリモーフィック関連は、1つの関連で複数のモデルを扱えるという柔軟さが魅力です。しかし、その柔軟さは同時に、アプリケーションやクエリ設計における複雑さを招きます。外部キー制約を貼れないため、データベースレベルでの整合性チェックが効きにくく、削除や更新時にはアプリ層での後処理や条件分岐が増えがちです。また、関連先ごとにテーブルが異なるため、JOINや eager_load が制限され、複雑なクエリや集計を1本のSQLで書きづらいという制約もあります。
関連先の種類が少ないうちは便利ですが、増えると保守コストが急上昇するので、気をつけながら使っていきたいところです。
参考記事
Discussion