はじめに
Railsでスーパータイプ・サブタイプの表現する方法は以下のように3つ存在します。
- 単一テーブル継承(STI)
- 具象クラス継承(CCI)
- クラステーブル継承(CTI)
Railsの機能としてSTIがサポートされているため、何も考えずにSTIを選んでしまうことが多い(かつては自分もそうだった)ような気がしますが、
それぞれメリット・デメリットがあるため適切に使い分けるといいことがあります。
まずはそれぞれの簡単な説明をしていきましょう。
それぞれの説明
ここでは例として、次のようなケースをかんがえてみます。(ちょっと無理やりですけどつっこまないで…)
Music というスーパータイプを持つ、 Classical と EDM という2つのサブタイプがあります。
Music は name 属性を持っています。これは当然2つのサブタイプ両方に必要な属性です。
また、Musicは抽象クラスであるとします。
各サブタイプに特有の属性としては Classical は conductor 、 EDM は bpm という属性を持ち、それぞれ必須な属性です。
単一テーブル継承
単一テーブル継承は、ひとつのテーブルにすべてのサブタイプを保存する方法です。関係ない属性はNULLで表現されます。
テーブル構造
Music
| id | type | name | conductor | bpm |
|---|---|---|---|---|
| 1 | Classical | 交響曲第6番 | ゴットフリート | NULL |
| 2 | EDM | Party People | NULL | 144 |
| 3 | Classical | ABC序曲 | アレッサンドロ | NULL |
アプリケーション側実装
ActiveRecordモデルのクラスに継承関係をもたせるだけです。
STIはフレームワークでサポートされているので、実装は容易です。
class Music < ApplicationRecord
# ...共通のコード
end
class Classical < Music
# ...固有のコード
end
メリット
- Railsが標準でサポートしているため、設計の難易度は低いと思われます。。
- サブタイプのレコードを取得するとき、JOINしたりする必要なく単純なSELECT文だけで全属性を取得できます。
-
name属性のようにスーパータイプの属性に対してUNIQUE制約をかけることができます。 - スーパータイプに存在するキーでサブタイプをまたいで検索をする際はクエリを一回発行するだけで済みます。
- サブタイプへの参照に外部キー制約を持たせることができます
デメリット
- あるサブタイプに固有の属性は、ほかのサブタイプの場合NULLが入ります。このため、
Classicだったらconductorは必ず入っていないといけない!という要件をDBのNOT NULL制約としてかけることができません。- ただし、
bpm属性のようにサブタイプ固有の属性にUNIQUE制約はかけることができます。(一部のDBMSを除きNULLはUNIQUE制約に縛られません) - ActiveRecordのレベルでバリデーションをかけることはできますが、同時に更新がおきたときなど突き抜けてしまうことがしばしばあります。。
- ただし、
- ほかのテーブルに特定のサブタイプだけを参照させる制約をもたせられません。(たとえば、
EDMとだけ関連付けたいDJが間違ってClassicalサブタイプのレコードを参照することをDBの制約では防げません。)
具象クラス継承
具象クラス継承は、具象クラスを保存するテーブルをそれぞれ作る方法です。
スーパータイプに対応するテーブルは存在せず(今回はMusicが抽象クラスなので)、サブタイプに対応するテーブルだけが作られます。
テーブル構造
Classical
| id | name | conductor |
|---|---|---|
| 1 | 交響曲第6番 | ゴットフリート |
| 2 | ABC序曲 | アレッサンドロ |
EDM
| id | name | bpm |
|---|---|---|
| 1 | Party People | 144 |
アプリケーション側実装
STIと違い、フレームワークでサポートされているものではないのでアプリケーション側の実装は設計次第です。なので、以下に上げているのは自分の考えた例でありもっといいやり方があるかもしれません。
たとえば、次のようにスーパータイプをActiveSupport::Concernモジュールとして定義して、各サブタイプのクラスでincludeするとか。スーパータイプ/サブタイプ→「継承だ!」と思ってしまいがちですが、必ずしもアプリケーション側に継承関係が現れるとは限りません。
module Music
extend ActiveSupport::Concern
included do
# ...Music共通のメソッドとかをかく
scope :hoge, -> {...}
def huga
...
end
end
end
class Classical < ApplicationRecord
include Music
# ...Classical特有のメソッドとかをかく
end
メリット
- サブタイプのレコードを取得するとき、単純なSELECT文だけで全属性を取得できます。
- サブタイプ固有の属性に対してNOT NULL制約をかけることができます。
- ほかのテーブルに特定のサブタイプだけを参照させる制約をもたせることができます
デメリット
- Railsフレームワークがサポートしていないので、アプリケーション側の設計が必要です。
-
name属性のようにスーパータイプの属性に対してUNIQUE制約をかけることができなくなってしまいます。(テーブルをまたいでしまうため、ClassicalとEDMに同じnameを持つレコードが入りうる) - スーパータイプに存在するキーでサブタイプをまたいで検索をするには、サブタイプのテーブルすべてをみる必要があります。(たとえば
Classical,EDMをまたいで"Party People"という名前の曲を探そうと思ったらそれぞれのテーブルにクエリする必要がある) - 別のモデルが複数のサブタイプを参照しうる場合、外部キー制約をかけられなくなります。(
Classical,EDMどちらとも関連しうるLikeには外部キー制約がかけられない)
クラステーブル継承
スーパータイプにある属性を保存するテーブルと、サブタイプ固有の属性を保存するテーブルを作ってリレーションを張ることによってスーパータイプ/サブタイプを表現します。
テーブル構造
Music
| id | name |
|---|---|
| 1 | 交響曲第6番 |
| 2 | Party People |
| 3 | ABC序曲 |
Classical
| id | music_id | conductor |
|---|---|---|
| 1 | 1 | ゴットフリート |
| 2 | 3 | アレッサンドロ |
EDM
| id | music_id | bpm |
|---|---|---|
| 1 | 2 | 144 |
アプリケーション側実装
こちらもアプリ側の実装はいろいろなやり方があるでしょう。以下も自分の考えた一例であり、もっといいやり方を教えてほしいくらいです。
特に、Railsはテーブル構造とモデルが密結合なので、サブタイプの属性に#attributesメソッドを呼び出したりしてもやりたいこととは違う結果になるでしょう。
class Music < ApplicationRecord
has_one :classical
has_one :edm
# ...Music共通のメソッドとかをかく
end
class Classical < ApplicationRecord
belongs_to :music
# ...Classical特有のメソッドとかをかく
# サブタイプに存在しないメソッドが呼ばれたらスーパータイプに委譲する
# def_delegators つかったほうがいいよね
def method_missing(name, *args)
music.send(name, *args)
end
end
依存関係を
メリット
-
name属性のようにスーパータイプの属性に対してUNIQUE制約をかけることができます。 - サブタイプ固有の属性に対してNOT NULL制約をかけることができます。
- スーパータイプに存在するキーでサブタイプをまたいで検索をする際はクエリを一回発行するだけで済みます。
- 他のモデルにスーパータイプのテーブルを参照させるかサブタイプのテーブルを参照させるか選ぶことができ、「特定のサブタイプへの参照が誤って他のサブタイプを参照することを防ぐ」か「参照に外部キー制約を持たせる」どちらかを選択することができます。
デメリット
- Railsフレームワークがサポートしていないので、アプリケーション側の設計が必要です。
- サブタイプのレコードを取得するとき、スーパータイプのテーブルとサブタイプのテーブルをJOINするかアプリケーション側で組み合わせる必要があります。
- 例にあげたような実装だと、1つのスーパータイプのレコードに複数のサブタイプからリレーションが張られることがありえます。アプリ側でおきないように頑張りましょう。
まとめ
それぞれの特徴を表にまとめると次のようになります。
| STI | CCI | CTI | |
|---|---|---|---|
| Railsがサポートしている | ○ | ||
| 単純SELECT文で全属性取得可能 | ○ | ○ | |
| スーパータイプの属性にUNIQUE制約をかけられる(1) | ○ | ○ | |
| サブタイプの属性にNOT NULL制約をかけられる | ○ | ○ | |
| 一回のクエリでスーパータイプに存在するキーでサブタイプをまたいで検索できる(2) | ○ | ○ | |
| 特定のサブタイプへの参照が誤って他のサブタイプを参照することを防ぐことができる(3) | ○ | △ | |
| サブタイプへの参照に外部キー制約を持たせることができる(4) | ○ | △ |
厳密に証明はできませんが、おそらく(1)と(2)は同値です。また、(3)と(4)は排他になります。CTIの場合はどちらかを選ぶことになります。
Railsがサポートしているからとりあえず・いつでもSTIを使うのではなく、それぞれの特徴を知った上で使い分けれられるようになるとよいですね。自分もそうなりたいです。
参考
- [みんなRailsのSTIを誤解してないか!?] (https://qiita.com/yebihara/items/9ecb838893ad99be0561)
