はじめに
チームとしてDB設計力をあげていかないとなぁと思っています。
1:1、1:N、N:Nの関連付けの設計や、正規化がしっかりとできるようになった後は、
単一テーブル継承(STI)について考え始めるタイミングかなと思います。
その時に備えて、ここに記しておこうと思います。
単一テーブル継承(STI)とは
STIを利用する目的は、複数の関連するクラスを単一のテーブルで管理する事により、
DB設計を単純にする事です。
Railsガイドに内容が簡潔に記載されていますが、もう少し噛み砕いて説明しようと思います。
STIを利用しない場合
class Car < ApplicationRecord
end
class Motorcycle < ApplicationRecord
end
class Bicycle < ApplicationRecord
end
DB設計
car
Field | Type |
---|---|
color | string |
price | integer |
motorcycle
Field | Type |
---|---|
color | string |
price | integer |
bicycle
Field | Type |
---|---|
color | string |
price | integer |
構造が同じで名前だけ異なるテーブルが3つ存在することになります。
共通化してDBテーブル数を減らしたい気持ちになります。
STIを利用する場合
Vehicleモデルは親です。
Carモデル、Motorcycleモデル、Bicycleモデルは子(サブクラス)です。
子(サブクラス)は、親を継承します。
class Vehicle < ApplicationRecord
end
class Car < Vehicle
end
class Motorcycle < Vehicle
end
class Bicycle < Vehicle
end
DB設計
vehicle
Field | Type |
---|---|
color | string |
price | integer |
type | string |
typeに入る値が、'Car'、'Motorcycle'、'Bicycle' に応じて、
RailsのModelが決定される形となります。
また、2通りのレコード作成方法があります。
- Carクラスでcreateする
Car.create(color: 'Red', price: 10000)
- typeに'Car'を指定してcreateする
Vehicle.create(color: 'Red', price: 10000, type: 'Car')
この方法により、DBテーブル数を1つに共通化する事ができます。
処理の共通化
各モデルで共通の処理があったとすると、それを親側で定義して共通化できます。
class Vehicle < ApplicationRecord
# 共通のバリデーション
validates :color, presence: true
validates :price, numericality: { greater_than: 0 }
# 共通のメソッド
def display_info
p "#{type}: Color - #{color}, Price - #{price}"
end
end
例えば、carインスタンスはVehicleを継承しているのでdisplay_infoを呼び出せます。
car.display_info # Car: Color - Red, Price - 10000"
特段、STIでなくても親Classを作成すれば処理の共通化はできるのですが、
STIを利用することで親側に処理を寄せようと感じるきっかけになれば、それはそれで良いかなと思います。
STIの課題
異なるサブクラスが異なる属性を持つ場合、多くのNULL値が発生する可能性があります。
vehicle
Field | Type | Description |
---|---|---|
type | string | 'car', 'motorcycle', 'bicycle' |
color | string | 共通属性 |
price | integer | 共通属性 |
doors | integer | car専用属性 |
engine_capacity | integer | motorcycle専用属性 |
has_basket | boolean | bicycle専用属性 |
この場合、typeが'Car'だと、engine_capacityとhas_basketはNULLになります。
Carにとっては不要な属性という訳です。
STIの使用が適切な場合
サブクラス間で多くの共通属性があり、異なる属性が少しだけ存在する場合にSTIを利用するのが良いと言われています。
また、オブジェクトのタイプ(type)が比較的少ない場合、という条件も必要でしょう。
typeが増えると、その分あるModelに対して不要な属性が肥大化する懸念があるためです。
弊社案件に関しては、そこまでtypeを大量に扱うような要件はなさそうなので、これについてはそこまで気にしなくて良いかなと思います。
最後に
どのテーブルならSTIを利用できるかな〜と考えるきっかけになれば良いなと思います。
因みに、Rails7.1からDelegated Typesという項目が追加されています。
本記事で述べたSTIの課題を解決するために編み出されたようですが、
少し複雑に見えるので、どうなのかな。。
次の記事の内容にするかどうかはもう少し考えよう。