7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

autosaveのときに属性に変更がなくても子のモデルでもsaveメソッドが必ず呼ばれるようにするには?

Last updated at Posted at 2018-02-18

要求

ネストしたリレーションを保存するような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

ここまで読んでくれてありがとう。

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?