要求
ネストしたリレーションを保存するようなviewで、子のオブジェクトで設定されたコマンドに応じて自身の属性を書き換えたい場合の処理を行う。
課題の認識
まずモデルを以下のように定義したとします。
class FooObject < ApplicationRecord
has_many :bar_objects, autosave: true, inverse_of: :foo_object
accept_nested_attrubites_for :bar_objects
end
class BarObject < ApplicationRecord
belongs_to :foo_object, inverse_of: :bar_objects
attr_accessor :commands
# コマンドに応じた処理
def handle_commands
# 属性をなんらか変更する
...
end
# こいつが怪しい
before_validation :handle_commands
end
このときcommandsは、attributesに存在しないためbar.commands = 'do your best'などと設定しても、attributesは変更を認識しません。
ということは、autosave: trueとしたところで、自動保存は動かないかもしれません。
なぜ、かも、、、なのかというと、なんらかの属性を設定した場合には、変更扱いになるので、before_validation以下が動きます。
なんとも、微妙な挙動なのです。
すぐに思いつく解決策は、以下のようにコントローラーの中で明示的にhandle_commandsを呼び出すというものです。
class FooObjectsController < ApplicationController
def update
@foo = FooObject.find(params[:id])
@foo.attributes = foo_object_params
@foo.bar_objects.to_a.select{ |b| b.commands.present? }.each(&:handle_commands)
@foo.save
end
end
まあ、これでもいいんですど、
これでは、コントローラーが仕様を知りすぎています。
rails consoleとか、他の例えばCSV一括取り込みなどで保存する場合も、いちいちこれをやらんといけません。
このhandle_commandsを呼び出すというのはbar_object固有の内部的な仕様です。
これはぜひともカプセル化したいので、BarObjectへ分離したいと思います。
ではどうする?
railsは、autosaveをtrueに設定した際に、railsが保存プロセスを動かすべきかどうかを判断するときに、changed_for_autosave?メソッドを使って確認します。
通常であれば、ありがたい挙動なのですが、before_validation、あるいはカスタムのコールバックを定義していて、そこで属性を再設定しているときなどはそれらすら呼び出されないので、困ります。
実際のコードは違いますが、ざっくり言えば、railsがやっていることは、
bar_objects.select(&:changed_for_autosave?).each(&:save)
のようなイメージです。
ですからこのメソッドをオーバーライドします。
class BarObject < ApplicationRecord
...
def changed_for_autosave?
super || @commands.present?
end
...
end
これで、@commandsになんらか設定されていれば、autosaveのときでも変更があったものとしてrailsは処理してくれるようになります。
最終的にControllerからコマンドに対する処理を取り除いて以下のようにできます。
class FooObjectsController < ApplicationController
...
def create
@foo = FooObject.find(params[:id])
@foo.attributes = foo_object_params
@foo.save
end
...
end
ここまで読んでくれてありがとう。