Help us understand the problem. What is going on with this article?

[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!みたいな形で作ってますが、もうちょいいいネーミングで作りたい。。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした