ポリモーフィック関連の罠と代替案
Rails 風 commentable_type + id の問題点と、STI / 型別 FK / Exclusive Arcs の代替案。
ポリモーフィック関連とは
「コメント」が「記事」にも「動画」にも「商品」にもつけられる、「添付ファイル」が複数のテーブルを参照する、などのケースで使われる設計パターンです。Rails (ActiveRecord) が広めた実装では、commentable_type (文字列で参照先テーブル名) と commentable_id (ID) の 2 列で「どのテーブルのどの行か」を表現します。
一見シンプルですが、リレーショナルモデルの最も基本的な仕組みである「外部キー」が使えなくなるのが最大の問題です。
-- 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 で書くことになり、数が増えると煩雑。
コメント数が種類ごとに大きく異なる場合や、種類ごとに機能差分があるなら、この分割が最もクリーンです。
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 個程度なら現実的です。
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 がやっているから」だけを根拠に採用するのは避けたい設計です。
