accepts_nested_attributes_forしたときのchanged?に気をつけよう

TL; DR

  • rails 5.0.2
  • accepts_nested_attributes_for で子モデルを変更しても、親モデルの changed?false を返すよ
  • なので、 changed? を見て何らかの処理をフックするようなコードを書くときは気をつけよう
  • relations_changed? メソッドを生やしておくのが良いと思うよ

検証コード

$ rails g model parent name:string
$ rails g model child parent:belongs_to name:string
class Parent < ApplicationRecord
  has_many :children
  accepts_nested_attributes_for :children, allow_destroy: true
end

class Child < ApplicationRecord
  belongs_to :parent
end

デフォルト挙動確認

そもそも、 accepts_nested_attributes_for とか関係なく、association先のモデルが変更されても、association元モデルの changed?false を返す。

# rails console
p = Parent.create(name: 'test1-p')
c = p.children.create(name: 'test1-c1')

p.changed?
=> false

c.name = 'changed'
p.changed?
=> false

この挙動は、 accepts_nested_attributes_for を使って子モデルの属性をセットした場合も変わらない。

# rails console
p = Parent.create(name: 'test2-p')

p.changed?
=> false

p.attributes = { children_attributes: [{ name: 'test2-c1' }] }

p.changed?
=> false

p.children[0].changed?
=> true

ミスりやすいポイント

「associationはもともとこういう挙動ですよ」と捉えると問題無さそうだが、最後の部分をStrongParameterっぽく書いてみると、かなり不思議な挙動に見えると思う。

p.attributes = parent_params
p.changed?
=> false

changed?true になるはずだ」と思ってしまってもしょうがない気がする。なので、要注意。

どんなコードを書くべきか

changed? メソッドをoverrideするという手もあるが、「accepts_nested_attributes_for に関係なく、 changed? はassociation先は見ない」という前提は覆さないほうが良いと思う。 というわけで、 children_changed? メソッドを Parent に生やそう。

# Parent.rb
def children_changed?
  children.any? do |c|
    c.new_record? || c.changed? || c.marked_for_destruction?
  end
end

最後の marked_for_destruction?accepts_nested_attributes_forallow_destroy: true オプションを有効にして、削除フラグを立てた場合に true を返してくれるメソッドなので、覚えておこう。