LoginSignup
35
25

More than 5 years have passed since last update.

Railsでスーパータイプ/サブタイプを表現する方法を比較してみる

Last updated at Posted at 2018-02-09

はじめに

Railsでスーパータイプ・サブタイプの表現する方法は以下のように3つ存在します。

  1. 単一テーブル継承(STI)
  2. 具象クラス継承(CCI)
  3. クラステーブル継承(CTI)

Railsの機能としてSTIがサポートされているため、何も考えずにSTIを選んでしまうことが多い(かつては自分もそうだった)ような気がしますが、
それぞれメリット・デメリットがあるため適切に使い分けるといいことがあります。

まずはそれぞれの簡単な説明をしていきましょう。

それぞれの説明

ここでは例として、次のようなケースをかんがえてみます。(ちょっと無理やりですけどつっこまないで…)

Music というスーパータイプを持つ、 ClassicalEDM という2つのサブタイプがあります。
Musicname 属性を持っています。これは当然2つのサブタイプ両方に必要な属性です。
また、Musicは抽象クラスであるとします。

各サブタイプに特有の属性としては ClassicalconductorEDMbpm という属性を持ち、それぞれ必須な属性です。

entity.png

単一テーブル継承

単一テーブル継承は、ひとつのテーブルにすべてのサブタイプを保存する方法です。関係ない属性はNULLで表現されます。

テーブル構造

Music

id type name conductor bpm
1 Classical 交響曲第6番 ゴットフリート NULL
2 EDM Party People NULL 144
3 Classical ABC序曲 アレッサンドロ NULL

アプリケーション側実装

ActiveRecordモデルのクラスに継承関係をもたせるだけです。
STIはフレームワークでサポートされているので、実装は容易です。

music.rb
class Music < ApplicationRecord
  # ...共通のコード
end
classical.rb
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するとか。スーパータイプ/サブタイプ→「継承だ!」と思ってしまいがちですが、必ずしもアプリケーション側に継承関係が現れるとは限りません。

music.rb
module Music
  extend ActiveSupport::Concern

  included do
    # ...Music共通のメソッドとかをかく

    scope :hoge, -> {...}

    def huga
     ...
    end
  end
end
Classical.rb
class Classical < ApplicationRecord
  include Music

  # ...Classical特有のメソッドとかをかく
end

メリット

  • サブタイプのレコードを取得するとき、単純なSELECT文だけで全属性を取得できます。
  • サブタイプ固有の属性に対してNOT NULL制約をかけることができます。
  • ほかのテーブルに特定のサブタイプだけを参照させる制約をもたせることができます

デメリット

  • Railsフレームワークがサポートしていないので、アプリケーション側の設計が必要です。
  • name 属性のようにスーパータイプの属性に対してUNIQUE制約をかけることができなくなってしまいます。(テーブルをまたいでしまうため、ClassicalEDMに同じ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メソッドを呼び出したりしてもやりたいこととは違う結果になるでしょう。

music.rb
class Music < ApplicationRecord
  has_one :classical
  has_one :edm

  # ...Music共通のメソッドとかをかく
end
Classical.rb
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を使うのではなく、それぞれの特徴を知った上で使い分けれられるようになるとよいですね。自分もそうなりたいです。

参考

35
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
35
25