Ruby on Railsでは単一テーブル継承しかサポートしていないが、クラステーブル継承・具象テーブル継承を実現しようと思ったときにアプリケーション側の実装はどんな感じが良いのか考えてみる。
継承とデータベース設計
まず前提。
アプリケーションの実装でクラス間に継承関係があった場合に、それをデータベース側で表現する方法として、PofEAAに以下3つが示されていて、RoRでは単一テーブル継承のみサポートされている。
- 単一テーブル継承
- クラステーブル継承
- 具象テーブル継承
RoRでの実装
テーブル関係をコードに落とそうと思ったとき、共通テーブルを親クラスとして定義して継承する方法と、共通テーブルの特徴をモジュール化してmixinする2つの方法が考えられると思う。両方とも共通の特徴をクラスに取り込むという意味では同じなのだが言語的には振る舞いが異なるのでそのあたりは下記参照。
https://qiita.com/pink_bangbi/items/2c2f17516cd3a7b4eeac
mixin
共通となる特徴をモジュール化して外部に切り出す方法。モジュールとして切り出して各モデルがモジュールをincludeすることで、コード的には関連するモデルが全て直接ActiveRecordを継承することになる。コーディングするときはuser.subscriptions
的に利用できる。
結論から言うと後述の継承を用いた方法はRails的にデメリットが多くて、mixinのほうがおすすめである。
module Chargeable
belongs_to :customer
has_many :subscriptions
...
end
class User < ActiveRecord::Base
include Chargeable
end
class Company < ActiveRecord::Base
include Chargeable
end
継承
RailsのSTIライクに共通属性を継承する形で利用できるようにする方法。他の言語でのオブジェクト指向プログラミングに慣れている場合は見た目的にもわかりやすくてしっくりくるかなと思う。
ただ、Rails的に結構トラップがあると思っているので注意も必要だったりする。
たとえば親クラスが外部に持つアソシエーションを子クラスで呼び出した場合に、子クラスの外部キーが用いられるたりする。(user.subscriptions
した場合、customer.id
を外部キーとしてsubscription
を取得してほしいところuser.id
に紐付いたsubscriptionsが取得されてしまう)
この場合以下のように、親クラスにdelegateして user.subscriptions
した場合も customer.subscriptions
が呼ばれるようにするなど対応が必要だと思う。
ほかにもたとえば、User
、Company
にポリモーフィック関連するモデルが存在した場合、hogerable_typeカラムに親クラスが設定されてしまうなど。
class Customer < ApplicationRecord
has_many :subscriptions
has_one :user
has_one :company
end
class User < Customer
belongs_to :customer
has_many :hoges, as: :hogerable
delegate :subscriptions, to: :customer
end
class Company < Customer
belongs_to :customer
has_many :hoges, as: :hogerable
delegate :subscriptions, to: :customer
end
class Hoge < ActiveRecord::Base
belongs_to :hogerable, polymorphic: true
end
結論
クラステーブル継承・具象テーブル継承するならmixinが無難(RubyやRails特有のリフレクションを使った実装がネックになるところはありそう)
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/inheritance.rb#L99RoRにおけるアクティブレコードパターンの実装を意識するとモデル継承による事故は減らせる