😇

ポリモーフィック関連のつらみ

に公開

ラブグラフでインターンをしているりょうさんです!

前回はポリモーフィック関連についてざっくり理解をしました。
https://zenn.dev/lovegraph/articles/8d965d2e19a132

今回はポリモーフィック関連について理解した上でつらい部分を紹介していこうと思います!
モデルや実際のコードは前回の記事(上記)を元に話していこうと思います!

以下のテーブルが存在する程で話します!

# 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 はポリモーフィックなので、実際には StudentDiaryClubBlog など、複数のテーブルが入り得ます。Rails はそれを動的に判断してクエリを分けてくれますが、joins で 1 つのテーブル に決め打ちすることはできません。

もし関連先の情報を取得するなら、データベースを JOIN せずに、クエリを発行する手順を考えます。となると考えられる手段は、includespreload ですね。個人的には、includes だとなんのクエリが走るのかが曖昧なので、preload の方が好みです。
こうすることにより、関連先が動的な場合でもデータを取得できます。

もしどうしてもJOINしたいケースや、JOINが業務要件として常態化するなら、ポリモーフィックをやめる設計変更が一番きれいになるでしょう。

まとめ

ポリモーフィック関連は、1つの関連で複数のモデルを扱えるという柔軟さが魅力です。しかし、その柔軟さは同時に、アプリケーションやクエリ設計における複雑さを招きます。外部キー制約を貼れないため、データベースレベルでの整合性チェックが効きにくく、削除や更新時にはアプリ層での後処理や条件分岐が増えがちです。また、関連先ごとにテーブルが異なるため、JOINや eager_load が制限され、複雑なクエリや集計を1本のSQLで書きづらいという制約もあります。
関連先の種類が少ないうちは便利ですが、増えると保守コストが急上昇するので、気をつけながら使っていきたいところです。

参考記事

https://railsguides.jp/association_basics.html

ラブグラフのエンジニアブログ

Discussion