はじめに
STIでtypeを変える方法。
動機
本来は禁じ手だが、STIのtypeを変えたい時がある。
STIのオブジェクトが他のモデルから関連付けられている場合などにおいて、削除->追加 をすると、アソシエーションの管理が面倒になる。
構成
- 『番号フォーマット』はある番号を生成するフォーマットであり、内容は複数の仕様のフォーマットで構成される。
- Time: strftimeのフォーマット (e.g. '%Y%m%d')
- Sequential: sprintfのフォーマット (e.g. '%04d')
- 『番号フォーマット』は、複数の『部分』から構成される
- 『部分』クラスは自分の仕様に従って文字列を生成する (日付、連番、etc..)
例) '%Y%m_Number%03d' # '201612_Number003' のように生成される
クラス
-
NumberFormat
ユーザへの公開クラス
NumberPartのリストを持つ。build()で各NumberPartのbuild()を呼び出し連携つする -
NumberPart
STIの親。 -
DatePart
STIの子。
build()で、登録されたフォーマットに従い文字列を生成する。フォーマットはstrftimeに準じる。 -
SequentialPart
STIの子。
build()で、登録されたフォーマットに従い文字列を生成する。フォーマットはsprintfに準じる。
連番を生成するため、オブジェクトは『現在の値』を管理する。
STIのtypeを変える
モデル。
STIでは各々の子クラスに必要なすべてのカラムを同一レコードで持つため、typeを変更したときに前のクラスのアトリビュートが残る。
つまり、type変更に伴って自分に必要なカラム・不要なカラムを適切に初期化する必要がある。
class NumberFormat < ActiveRecord::Base
has_many :number_parts
...
def build
number_parts.map{|np| np.build()}.inject(&:+)
end
class NumberPart < ActiveRecord::Base
# id :integer
# type :string # STI
# format :string # 番号フォーマット
# current_number :integer # NumberPartが使う『現在の値』
...
belongs_to :number_format
after_save :reset
def build
''
end
private
# 親クラスで全てのSTIの子に特異なアトリビュートをリセット
def reset
update_column(:current_number, nil)
end
class DatePart < NumberPart
def build
Time.zone.now.strftime format
end
class SequentialPart < NumberPart
# STIのtype変更時にパラメータをリセットする必要のある必要な子クラスは
# 初期化をオーバーライドする。
after_save :reset
def build
num = format % current_number
increment! :current_number
num
end
def reset
update_column(:current_number, 0) unless self.current
end
変更するコード
class HogeHogeController
...
def transform_parts_type(number_part, new_type_name)
# まずtypeのみDBレコード直で書き換える
number_part.update_column(:type, new_type_name)
# becomes!でクラスを変更する
type_updated = number_part.becomes!(new_type_name.constantize)
# モデルのsaveコールバックを発生させ、タイプ変更時の初期化を行う
type_updated.save
end
図作成