sky’s 雑記

露頭に迷うエンジニア雑記

クラス継承とデータベース設計

一応下記の続き的な立ち位置

iwsksky.hatenablog.com

オブジェクト指向言語でリレーショナルデータベースとモデルをつなぐオブジェクトマッピングで、 どのように継承関係を表すかというのは結構難しい話題なのだが、 それについてちょっと書いてみたいと思う。

継承とデータベース設計

アプリケーションの実装でクラス間に継承関係があった場合に、それをデータベース側で表現する方法として、 PofEAAに以下3つが示されている。

  • 単一テーブル継承

単一テーブル継承

  • クラステーブル継承

クラステーブル継承

  • 具象テーブル継承

具象テーブル継承

ちなみに今回はRoRの実装をしていて感じたことなのでRoR前提で記す。

ActiveRecordの難しさ

RoRActiveRecordはその名の由来にもなっているように、 データベースとアプリケーションをつなぐアーキテクチャのうち、 アクティブレコードパターンで実装されている。

そしてアクティブレコードパターンは他のパターンに比べてアプリケーションとテーブルが密結合である。 (これについては同じくPofEAAの10章参照) ActiveRecord

実装しているうちは難しさを感じるものの、それを言語化できなかったのだが、 アクティブレコードを使った実装の難しさは密結合であるがゆえに、 アプリケーションとテーブルどちらを優先して考えたらいいかわからなくなるということだと思う。

具体的にそれぞれを優先した場合のpros/consを書いてみたいと思う。

アプリケーション優先

フレームワークORMによって違うと思うが、 RoRでは継承の表現として単一テーブル継承のみサポートしている。 単一テーブル継承は上述のとおり1テーブルで継承クラスを表現する、 具体的にはtypeカラムをテーブルに持たせて各クラスの差分についてはnullになるというもの。 概念としては異なるがテーブル自体はポリモーフィック関連と似たものになると思う。

そしてRoRに限定した場合、アプリケーションを優先させることはSTIを採用することと言っていいと思う。

pros
  • RoRでサポートされているのでアプリケーション側の実装が容易である
  • 単一テーブルなのでテーブル間での参照が生じずシンプルなクエリ発行で済む
cons
  • 完全新規の実装でない場合データベース側のスキーマの変更が生じる (user:個人とcompany:法人というテーブルがもともと存在し、その両者に課金機能をもたせたい場合、これらテーブルをマージしてtypeカラムを追加する必要がある)
  • 外部キー制約がかけられないので外部テーブルとの連携に弱い
  • nullカラムが増えるのでテーブルがスパースになる
  • typeカラムがカーディナリティ低くなりがちなのでクラスごとに絞り込みを行うことにつらみがある
  • (なんとなく)STIを直感的に気持ち悪いと感じる開発者が多い

データベース優先

STI/CTI/CCIどれが優れたパターンかという話ではなく、 アプリケーション側の事情を考慮せずデータベース設計を行うという話。 STIを用いない場合ある程度自力での実装が必要になるがその場合はアプリケーション側で吸収する。

pros
  • データベース側に変更が生じないので大掛かりになりにくい
  • (なんとなく直感的に綺麗だと感じる)CTI/CCIが選択できる
cons
  • 共通カラムを参照するような基本的なレコードであってもジョインが必要になる
  • スーパークラスを継承するクラスが増えたときにアプリケーションのコードが煩雑になりやすい
RoRでの実装

STIの実装例はググればいくらでも出てくるので、参考までに継承を使った場合の実装を書いておく。 気をつけなければならないのは、親クラスが外部に持つアソシエーションを子クラスで呼び出した場合に、 子クラスの外部キーが用いられること。 なので親クラスに定義したアソシエーションを子クラスで呼び出した場合は子クラスのidが外部キーとして使用される。 親クラスにdelegateして user.subscriptions した場合も customer.subscriptions が呼ばれるようにするなど対応が必要だと思う。

class Customer < ApplicationRecord
  has_many :subscriptions
  has_one :user
  has_one :company
  
end
class User < Customer
  belongs_to :customer
  delegate :subscriptions, to: :customer
end
class Company < Customer
  belongs_to :customer
  delegate :subscriptions, to: :customer
end

結論

どっち優先で考えるかは実装と可用性のバランスをどう取るかかなと思った。 ことRoRにおいてはCTI/CCIを選択したとしてもそれほどつらみを感じないので選択肢としてはありなのかなと思う。 また実装者が違和感を感じるか、というのは結構重要な観点で、STIに違和感を感じる実装者がいるのもまた事実、実際のところとくに問題のないアーキテクチャだとしても違和感を潰すこととか違和感を感じないものを選択することもあわせて重要なんだろうなと思う。

ref

この辺も参考になると思う。 github.com