Edited at

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

More than 1 year has passed since last update.

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!みたいな形で作ってますが、もうちょいいいネーミングで作りたい。。