データモデル設計ガイド

ポリモーフィック関連の罠と代替案

Rails 風 commentable_type + id の問題点と、STI / 型別 FK / Exclusive Arcs の代替案。

ポリモーフィック関連の罠と代替案 diagram

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

「コメント」が「記事」にも「動画」にも「商品」にもつけられる、「添付ファイル」が複数のテーブルを参照する、などのケースで使われる設計パターンです。Rails (ActiveRecord) が広めた実装では、commentable_type (文字列で参照先テーブル名) と commentable_id (ID) の 2 列で「どのテーブルのどの行か」を表現します。

一見シンプルですが、リレーショナルモデルの最も基本的な仕組みである「外部キー」が使えなくなるのが最大の問題です。

sql
-- Rails スタイルのポリモーフィック関連
CREATE TABLE comments (
  id               BIGSERIAL   PRIMARY KEY,
  body             TEXT        NOT NULL,
  commentable_type VARCHAR(50) NOT NULL,  -- 'Article' / 'Video' / 'Product'
  commentable_id   BIGINT      NOT NULL
  -- FK が張れない!
);

問題点 1: 外部キー制約が機能しない

外部キーは「参照先テーブルがただ 1 つ」決まっていることが前提です。commentable_type によって参照先が動的に変わる構造では、DB レベルで整合性を保証できません

  • 存在しない Article#99999 へのコメントが作れてしまう
  • 記事が削除されてもコメントが孤立する(CASCADE が機能しない)
  • タイポした commentable_type = 'article'(大文字小文字違い)で参照不能な行が生まれる

整合性は完全にアプリコード任せになり、バッチ処理や raw SQL 経由の更新で簡単に壊れます。

問題点 2: JOIN が複雑でパフォーマンスが出ない

「コメントと参照先を一緒に取得する」クエリが書きにくくなります。

  • 1 回の SQL で済ませるには、参照先ごとに LEFT JOIN を並べて CASE で結合する必要がある
  • ORM 任せにすると N+1 クエリになり、1 ページで 100 クエリ飛ぶような事態を招く
  • オプティマイザも commentable_type = 'Article' の分岐をうまく扱えず、全参照先テーブルを読みがち

「記事ページに最新コメントを表示」程度の処理で、隠れたパフォーマンス問題を抱えやすいパターンです。

代替案 1: STI (Single Table Inheritance)

参照されうるテーブル(記事・動画・商品)を1 つのテーブル commentables に統合し、type 列でサブクラスを区別する方式です。コメントはこの 1 テーブルを FK 参照するだけで済みます。

利点: FK が張れる、JOIN が単純。欠点: 列が混在するため NULL が増える、各サブクラス固有の列をうまく扱えない。記事と動画の共通項が多い場合には有効です。

代替案 2: 参照先ごとに別テーブル

「記事のコメント」「動画のコメント」「商品のコメント」を別テーブルに分ける方式です。article_comments (article_id, body) / video_comments (video_id, body) のように、それぞれ通常の FK を張ります。

利点: 整合性完璧、クエリも単純。欠点: 「全種類のコメントを時系列で表示」のようなクエリが UNION で書くことになり、数が増えると煩雑。

コメント数が種類ごとに大きく異なる場合や、種類ごとに機能差分があるなら、この分割が最もクリーンです。

sql
CREATE TABLE article_comments (
  id         BIGSERIAL PRIMARY KEY,
  article_id BIGINT    NOT NULL REFERENCES articles(id) ON DELETE CASCADE,
  body       TEXT      NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE video_comments (
  id         BIGSERIAL PRIMARY KEY,
  video_id   BIGINT    NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
  body       TEXT      NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

代替案 3: Exclusive Arcs (排他的外部キー)

コメントテーブルに 参照先ごとの FK 列を複数用意し、同時に 1 つだけ NOT NULL になるよう CHECK 制約で強制する方式です。

利点: FK が機能し、1 テーブルで完結、UNION も不要。欠点: 参照先が増えると列数も増え、CHECK 制約が長くなる。参照先が 3〜5 個程度なら現実的です。

sql
CREATE TABLE comments (
  id         BIGSERIAL PRIMARY KEY,
  body       TEXT      NOT NULL,
  article_id BIGINT    NULL REFERENCES articles(id) ON DELETE CASCADE,
  video_id   BIGINT    NULL REFERENCES videos(id)   ON DELETE CASCADE,
  product_id BIGINT    NULL REFERENCES products(id) ON DELETE CASCADE,
  -- 同時に 1 つだけ NOT NULL
  CHECK (
    (CASE WHEN article_id IS NULL THEN 0 ELSE 1 END)
  + (CASE WHEN video_id   IS NULL THEN 0 ELSE 1 END)
  + (CASE WHEN product_id IS NULL THEN 0 ELSE 1 END) = 1
  )
);

いつポリモーフィック関連でも許容できるか

それでも Rails スタイルのポリモーフィックを選ぶ場合の条件:

  • 参照先が非常に多い(例: 10 種類以上)かつ種類が増え続ける
  • 整合性の厳密さより開発速度が明確に重要
  • アプリが単一で、生 SQL でデータをいじることがない

それ以外のほとんどのケースでは、「テーブル別に分ける」「Exclusive Arcs」のどちらかが上位互換です。「Rails がやっているから」だけを根拠に採用するのは避けたい設計です。