RailsのSTIではtypeカラムにstring型でサブクラス名を保存する。
これは新しく0から設計する場合は良いかもしれない。
しかし既に次に述べるようなテーブルがある場合はintegerで関連付けられるほうが嬉しい。
Productテーブルが存在し、Product.category_idなどのカラムを使ってCategoryテーブルと紐付いているとする。
Product belongs_to Categoryの状態である。
class Product < ApplicationRecord
belongs_to :category, optional: true
end
class Category < ApplicationRecord
has_many :products
end
ここでCategoryごとにProductのサブクラスを作りたくなるとする。
今回の場合はCategoryがcpuやgpuなどである。
class Cpu < Product
end
普通に新しく設計する場合、もしくは冗長性を許して新しくtypeカラムを作る場合は(typeカラムにcpuやgpuなどのstring型が保存され、category_idに1, 2などのint型が保存される)
単にtypeカラムを作ってあげるだけでよい。
今回は既存のcategory_idを利用してSTIを実現する。
class Product < ApplicationRecord
self.inheritance_column = 'category_id'
belongs_to :category, optional: true
class << self
def find_sti_class(category_id)
type_name = category_names[category_id - 1]
"#{type_name.to_s.camelize}".constantize
end
def sti_name
category_names.index(name.demodulize) + 1
end
end
def self.category_names
['Cpu', 'Gpu']
end
end
self.inheritance_column
にcategory_idを代入することで、今までrailsでtypeを用いてSTIを実現していたのが、category_idというカラムで実現できるようになる。
続いて子クラスの探索を行うActiveRecord::Inheritance(inheritance.rb)のfind_sti_classとsti_nameをオーバーライドする。
これにより①category_idというinteger型から必要なクラス名を取得することと②子クラス名からcategory_idを逆算することが可能になる。
実際にテストしてみると、
product = Product.where(category_id: 1).limit(1)[0] # cpu
p product.category_id # 1
p product.category # #<Category id: 1, name: "CPU">
p product.class.name # Cpu
gpu = Gpu.limit(1)[0] # #<Gpu id: *, name:...>
というように既存のリレーションを維持したまま、既存のカラムを利用してSTIが可能となった。