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