LoginSignup
21
23

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-12-24

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
23