DB設計のアンチパターン『ポリモーフィック関連』をあえて採用した話

2025/12/24 公開
このエントリーをはてなブックマークに追加

はじめに

この度、DB の論理設計でポリモーフィック関連を採用したところ、レビューで「それはアンチパターンだよ」とご指摘いただきました。
そもそも、ポリモーフィック関連がアンチパターンとされること自体を知らなかったことが原因です。
これを機にポリモーフィック関連について学習したうえで、最終的にはこの設計を本採用することにしました。この過程で学んだことを共有します。

実際の事例

今回の事例では、外部連携した商品について、紐づく商品がどんな種類であっても同じように扱いたいがために、下記のような外部連携商品(ExternalItems)テーブルを設計しました。

  • 複数の種類の商品に対する外部キー(linkable_id)を使用します。
  • 加えて、文字列型のlinkable_type列を定義して、どの種類の商品を参照するかのメタデータを格納します。具体的にはコア商品CoreItem、限定商品LimitedItemなどが入る想定です。
    この論理設計はポリモーフィック関連と呼ばれ、書籍『SQLアンチパターン』ではアンチパターンとされています*1。

なぜアンチパターンか

a. 外部キー制約を定義できない

外部キー制約は1つのテーブルしか指定できません。よって、linkable_idが親テーブルの値と一致するかをDBレベルで担保できません。

b. ER図で関連を表現できない

ER図で動的な参照は表現できません。ER 図を見たときに関連性がわかりづらく、後から設計を理解する人に不親切になります。

c. 親テーブルの情報を取得するクエリの実行が辛い

外部連携商品名の一覧が欲しい場合のクエリは下記になります。

SELECT e.*, c.name, l.name
FROM ExternalItems AS e
  -- コア商品の結合
  LEFT OUTER JOIN CoreItems AS c
    ON (e.linkable_id = c.id AND e.linkable_type = 'CoreItem') 

  -- 限定商品の結合
  LEFT OUTER JOIN LimitedItems AS l
    ON (e.linkable_id = l.id AND e.linkable_type = 'LimitedItem');

親テーブルが増えるごとにJOINを書かなければならなくなります。

また、実行結果は下記のように、どちらか一方の列が常に NULL になる形になります。

c.name (コア商品名)

l.name (限定商品名)

バナナ

NULL

NULL

秋限定の柿

りんご

NULL

アプリケーションコードで「どのカラムに値が入っているか」を判定しなければならなくなります。また、ソートやフィルタリングも工夫する必要が出てきます。

ORDER BY COALESCE(c.name, l.name) -- COALESCE等でNULLじゃない方を探す

関数を使用する場合は、関数インデックスなどを張らない限りは基本的に通常の INDEX が効かないため、パフォーマンスにも影響がありそうです。

一般的な解決策

親ごとに交差テーブルを作成する

コア商品テーブルとの間に交差テーブル(CoreExternalItems)を作成します。


これを限定商品等、その他の商品の種類毎作成します。

a. 外部キー制約が定義できるようになる
外部キー制約をつけられるようになりDBレベルでidの値の一致を担保できるようになりました。

b. ER図で関連を表現できるようになる
上の図のように静的な関連を表現できます。

c. 親テーブルの情報を取得するクエリの実行は辛いまま

SELECT e.*, c.name, l.name
FROM ExternalItems AS e
  -- コア商品の結合
  LEFT OUTER JOIN CoreExternalItems AS ce
    ON e.id = ce.external_item_id
  LEFT OUTER JOIN CoreItems AS c
    ON ce.core_item_id = c.id

  -- 限定商品の結合
  LEFT OUTER JOIN LimitedExternalItems AS le
    ON e.id = le.external_item_id
  LEFT OUTER JOIN LimitedItems AS l
    ON le.limited_item_id = l.id;

linkable_typeの指定をしなくて良くなりましたが、親テーブルが増えればJOINを増やす必要性は変わらなそうです。結果セットにもNULLが入ります(親種別ごとに UNION ALL でまとめる方法はある)。

共通の基底テーブルを作成する

コア商品と限定商品などのすべての商品が継承する基底テーブルを作成します。



a. 外部キー制約を定義できるようになる
こちらも、外部キー制約をつけられるようになります。

b. ER図で関連を表現できるようになる
上の図参照。

c. 親テーブルの情報を取得するクエリがシンプルになる
外部連携商品名の一覧を取得するクエリはどうでしょうか。

SELECT e.*, i.name
FROM ExternalItems e
  JOIN Items i ON e.item_id = i.id; -- 結合先は常にItemsテーブル1つだけ

結合先がItemsテーブルのみになり、クエリがシンプルになりました。取得したレコードごとにNULLが現れることもないので、アプリケーション側で判定ロジックが不要になります。また、INDEXを効かせたソートやフィルタリングも可能になります。

最終的にポリモーフィック関連を採用した理由

データ構造的に既存の商品テーブルに共通の基底テーブルを導入するコストが高かった

マイグレーションの手間だけでなく、既存処理への影響範囲も広く、今回の機能追加のスコープとしては現実的ではないと判断しました。

外部連携商品の振る舞いはどの商品でも同じだった

要件を整理すると、外部連携機能として必要なのは「外部連携商品として同じように扱えること」であり、その実態は意識する必要がありませんでした。よってポリモーフィック関連の動的な参照でも成立すると判断しました。

Eloquent のサポートによるメリットが大きいと判断した

このアプリケーションはバックエンドのフレームワークにLaravelを使用しており、LaravelのEloquentにはポリモーフィック関連を扱う機能がサポートされています*2。

class ExternalItem extends Model
{
    public function linkable()
    {
        return $this->morphTo(); // CoreItem, LimitedItem などを透過的に扱える
    }
}

これにより、下記のメリットが得られます。

  • 交差テーブル案に比べて、テーブル数・モデル数・リレーション定義を抑えられる
  • with('linkable') のように直感的な書き方でリレーションをロードできる
  • CRUD 処理や関連付けのコード量を減らせる

フレームワークへの依存度は上がりますが、このプロジェクトでは今後もLaravelを使い続ける予定であり、開発効率を優先するという判断をしています。

まとめ

本記事では、私の事例をみながら、ポリモーフィック関連の概要と採用した理由について書きました。もちろん、RDB のお作法だけを見ればポリモーフィック関連は推奨されない設計です。それでも今回は、要件・既存スキーマ・フレームワークの制約を踏まえたうえで、
「デメリットを理解したうえであえて選ぶ」判断をしました。
メリット・デメリットを理解したうえであれば、ポリモーフィック関連も1つの選択肢となり、状況によっては今回のように採用できるかもしれません。
参考にしていただけると幸いです!


*1: Bill Karwin 著、和田 卓人 監訳、児島 修 訳 『SQLアンチパターン 第2版』オライリー・ジャパン(2025)

*2: Laravel 公式ドキュメント「Eloquent: Relationships(Polymorphic Relationships)」, Laravel 12.x