STIのモデルのデータ移行・リファクタリングみたいなことをした際に得られたSTIをいじくり回す知見を書き留めておく
環境
ruby 3.2.2
Rails 7.0.8
前提
class Person < ApplicationRecord
self.store_full_sti_class = false
end
class Person
class Child < Person
end
end
みたいなモデルがあるとする
クラス名とsti_name
を別のものにしたい時は
class Person < ApplicationRecord
self.store_full_sti_class = false
class << self
def find_sti_class(type_name)
case type_name
when 'Baby'
Person::Child
else
super(type_name)
end
end
end
end
class Person
class Child < Person
class << self
def sti_name
'Baby'
end
end
end
end
こうすることでtype
がBaby
のレコードをPerson::Child
として扱うことができる
解説
sti_name
をオーバーライドするだけだとDBのレコードを取得してインスタンス化するときに「Baby
に対応するクラスPerson::Baby
がない」というエラーが出るのでfind_sti_class
をオーバーライドしてBaby
とPerson::Child
のマッピングをする必要がある
レコード検索時に発行されるSQLのWHERE ***.type = '****'
を任意の値に変えたい時は
class Person < ApplicationRecord
self.store_full_sti_class = false
class << self
def find_sti_class(type_name)
case type_name
when 'Baby'
Person::Child
else
super(type_name)
end
end
end
end
class Person
class Child < Person
class << self
def type_condition(table = arel_table)
sti_column = table[inheritance_column]
sti_names = ['Baby', 'Child']
predicate_builder.build(sti_column, sti_names)
end
end
end
end
こうすることでtype
がBaby
のレコードとChild
のレコードの両方をPerson::Child
として扱うことができる
解説
type_condition
というメソッドがレコード検索時のtype
の絞り込みをしているのでそこをオーバーライドすることでWHERE type IN ('Baby', 'Child')
というSQLになる
ただし、それだけだとDBのレコードを取得してインスタンス化するときに「Baby
に対応するクラスPerson::Baby
がない」というエラーが出るので併せてfind_sti_class
をオーバーライドしてBaby
とPerson::Child
のマッピングをする必要がある
ちなみに
class Person
class Child < Person
default_scope { unscope(where: :type).where(type: %i[Baby Child]) }
end
end
ってやるとぱっと見動くけどjoinした時や、アソシエーションでの参照時のクエリが意図した挙動にならなかったです
〆
RailsのコードをオーバーライドするというパワープレイはRailsのバージョンを上げる際の妨げになりやすいので一時的な対応としてのみ使いましょう