テーブルカテゴライズ
業務でテーブル設計をレビューしていただいて、色々考えることがあったので、例え話で書きます。
繰り返しますが全て例え話です。
前提
- Appleの製品をRDBMS上でテーブルとして扱いたい。
- 扱いたい製品はiPhoneとiPadの2種類で、それぞれシリーズによって分類したい。
- 製品はロジック上は違いを意識したくない。
- 製品情報はシステム上では末端の情報で、他のロジックに対して及ぼす影響は少ない。
このようなシステム要件に対して、以下のような設計をして、案1として提案した。
apple_products
テーブル
column_name | type | description |
---|---|---|
id | int | なし |
purchase_date | date | 購入日 |
product_type | int | 1: iPhone, 2: iPad |
iphones
テーブル
column_name | type | description |
---|---|---|
id | int | なし |
apple_produt_id | int | apple_productのid |
product_type | int | 1固定 |
series | int | 1: iPhone6, 2: iPhone6S, 3: iPhone7 |
serial_number | text | シリアルナンバー |
phone_number | text | 電話番号 |
ipads
テーブル
column_name | type | description |
---|---|---|
id | int | なし |
product_type | int | 2固定 |
apple_produt_id | int | apple_productのid |
series | int | 1: iPad, 2: iPad mini, 3: iPad Pro |
serial_number | text | シリアルナンバー |
apple_products
の1つ1つのレコードにはiphones
のレコードかipads
のレコード、
どちらか1つがくっついている、という構成。
Rails上の実装はこんな感じをイメージしていた。
class AppleProduct < ActiveRecord::Base has_one :iphones has_one :ipads def product return iphone if product_type == 1 ipads if product_type == 2 end end
この設計がどうか、という相談をしたら、
以下のようにしたらいいのでは、というお話をいただいた。
これを案2とする。
apple_products
テーブル
column_name | type | description |
---|---|---|
id | int | なし |
purchase_date | date | 購入日 |
specific_products
テーブル
column_name | type | description |
---|---|---|
id | int | なし |
apple_produt_id | int | apple_productのid |
product_type | int | 1: iPhone, 2: iPad |
series | int | 1: iPhone6, 2: iPhone6S, 3: iPhone7, 4: iPad, 5: iPad mini, 6: iPad Pro |
serial_number | text | シリアルナンバー |
phone_number | text | 電話番号 ない場合は空文字 |
つまり、「iPhoneもiPadもいっしょくたにして1つのテーブルにしてしまえば?」というお話。
この例ではiphones
テーブルとipads
テーブルのカラムの違いはphone_number
だけだが、
実際に業務で使っているテーブルはカラムの違いが他にもあって、
今回の例に照らすと、specific_products
にipad
の情報を入れたら、
そこそこの数のnull
なり空文字なりが入って歯抜け情報になってしまうという状況。
それでもこっちの構成のほうが楽じゃない?と言われて、理由が全然わからなかった。
歯抜けの情報があるって気持ち悪いし、別のものなら別のものとしてテーブルを分けるのが自然な発想なんじゃないの?って。
で、理由を訊いてみたら以下のような内容だった。
apple_products
のレコードが、自分の持っているproductの情報を知らなくてすむ。
案1だと、自分の持っているproductがiphone
なのかipad
なのかをapple_product
が知っていることになる。それはapple_product
にとってはどうでもいいこと。- RDBMSの観点から明らかに案2の方がシンプル。Rails上でも
AppleProduct.includes(:specific_product)
と書ける。
案1では、DBに問い合わせして結果が返ってくるまで各apple_products
レコードがiphone
,ipad
どちらを持っているのかわからないので、無意識にjoin
したりinclude
したりできない。 - 確かに
null
や空文字など歯抜けの情報はあるが、それはRails上でうまいことやればよい。
これらの理由は納得できるものだったので、最終的に案2の方針でいくことに決めた。
持ち帰って改めて考えてみたけど、今まで自分の中に「設計はできるだけ実体に即したものにする、もしくはがんばってそれに近づける」という発想しかなくて、
「データとして楽に扱えるようにする」という発想がなかったと思った。
「システムとしてはこういうものができていればいいね」という段階までビジネス要件が落とし込んであれば、
設計はエンジニアが構成を見てどういうものなのかがわかれば良くて、あとはデータとして楽に扱える、ということを主眼において考えればよいのかなと思う。
案2のRails上の実装も考えてみたけど、こんなかんじになるだろうか。
apple_product.rb
class AppleProduct < ActiveRecord::Base has_one :specific_product def product specific_product.product end end
specific_product.rb
class SpecificProduct < ActiveRecord::Base belongs_to :apple_product def product return IPhone.new if product_type == 1 IPad.new if product_type == 2 end def iphone IPhone.new end def ipad IPad.new end class IPhone attr_reader :product_type, :type, :searial_number, :phone_number def initialize @product_type = product_type @series = series @searial_number = searial_number @phone_number = phone_number end end class IPad attr_reader :product_type, :type, :searial_number, :phone_number def initialize @product_type = product_type @series = series @searial_number = searial_number @phone_number = "-" end end private_constant :IPhone, :IPad end
別の話になるけど、色々考えてるうちに、ORMとしてのActiveRecord
って何を隠蔽してるんだ?と思った。
Rails上でレコードをオブジェクトとして扱えるっていうのは便利だと思うけど
includes
とかするなら結局テーブル構成見なきゃいけないし、SQL文がちょっと楽に書けるようになっているだけでは…?