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のバージョンを上げる際の妨げになりやすいので一時的な対応としてのみ使いましょう