Rails
ActiveRecord
STI
polymorphic

[Rails]STIから脱却してCTIでポリモーフィックを実現する

Ruby on Rails Advent Calendar 2017の24日目の記事です。

SQLアンチパターンやPofEAAで「オブジェクト指向設計で抽出されたスーパークラス・サブクラスから成る継承階層をリレーショナルデータベースのテーブルとして実装するためのパターン」として具象テーブル継承、クラステーブル継承、単一テーブル継承(STI)の3つが紹介されています。
みんなRailsのSTIを誤解してないか!?

その中でRailsはSTIはサポートされてますが、その他2つは自分で頑張ってポリモーフィックを実現しないといけないです。

STIを使わない理由としては

  • NULLを許容したくない
  • UpdateではなくInsertでデータ保存のフローを作りたい
  • has_manyなデータはSTIだとjson型などスキーマレスになる

あたりでしょうか。
今回はクラステーブル継承を実現する方法を書きます。データベースはMySQLを使うので、クラステーブル継承をデータベースがサポートしてない前提です。

migration

migrate/create_customers.rb
class CreateCustomers < ActiveRecord::Migration[5.1]
  def change
    create_table :customers do |t|
      t.integer :type, null: false
      t.string :code, null: false
      t.string :name, null: false

      t.timestamps
    end
  end
end
migrate/create_persons.rb
class CreatePersons < ActiveRecord::Migration[5.1]
  def change
    create_table :persons do |t|
      t.belongs_to :customer, foreign_key: true, null: false
      t.string :real_name, null: false

      t.timestamps
    end
    add_index :persons, :customer_id, unique: true
  end
end
migrate/create_corporations.rb
class CreateCorporations < ActiveRecord::Migration[5.1]
  def change
    create_table :corporations do |t|
      t.belongs_to :customer, foreign_key: true, null: false
      t.integer :corporate_number, null: false

      t.timestamps
    end
    add_index :corporations, :customer_id, unique: true
  end
end

MySQLはクラステーブル継承をサポートしてないので、customerにtypeをサブクラスのテーブルにcusotmerへのリレーションを貼ります。

モデル

models/customer.rb
class Customer < ApplicationRecord
  has_one :person
  has_one :corporation

  enum type: %i[person corporation]

  def do_somthing
    raise "must be override"
  end

  def cast
    self.becomes("#{self.class.to_s.pluralize}::#{type.camelcase}".constantize)
  end
end
models/person.rb
class Person < ApplicationRecord
  belongs_to :customer
end
models/corporation.rb
class Corporation < ApplicationRecord
  belongs_to :customer
end
models/customers/person.rb
module Customers
  class Person < Customer
    def do_somthing
      # do anything
    end
  end
end
models/customers/corporation.rb
module Customers
  class Corporation < Customer
    def do_somthing
      # do anything
    end
  end
end

castでmodels/customers/**.rbに変えています。

Customer.find(id).cast.do_somthing

でポリモーフィックを実現してます。

その他

  • 委譲するのもあり。その場合はファクトリクラスとmessages/**.rbをactive recordではない別のクラスとして定義すれば良い。messages/**.rbもmodelsにおく必要はないかも。
  • createする時はCustomer.create_all!などを作って、まずcusotmerを作成、その後にcastしてcreate_sub!みたいな形で作ってますが、もうちょいいいネーミングで作りたい。。