ぜったいってなに

Software Engineerのブログです。

テーブルカテゴライズ

業務でテーブル設計をレビューしていただいて、色々考えることがあったので、例え話で書きます。


繰り返しますが全て例え話です。



前提

  • Appleの製品をRDBMS上でテーブルとして扱いたい。
  • 扱いたい製品はiPhoneiPadの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つがくっついている、という構成。

f:id:zettaittenani:20170909162523p:plain


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 電話番号 ない場合は空文字


つまり、「iPhoneiPadもいっしょくたにして1つのテーブルにしてしまえば?」というお話。


f:id:zettaittenani:20170909163256p:plain


この例ではiphonesテーブルとipadsテーブルのカラムの違いはphone_numberだけだが、 実際に業務で使っているテーブルはカラムの違いが他にもあって、 今回の例に照らすと、specific_productsipadの情報を入れたら、 そこそこの数の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文がちょっと楽に書けるようになっているだけでは…?