7

I have an after_save callback on a model, and I'm calling previous_changes to see if an attribute (is_complete) changed. Even when the attribute changes, previous_changes returns an empty hash.

Here's the callback:

after_save do |record|
  puts "********************"
  puts record.previous_changes.to_s
  puts record.is_complete
  puts "********************"
end

and here's what I get in the log:

********************
{}
true
********************
********************
{}
false
********************

If the value of is_complete changed from true to false, it should be in the previous_changes hash. The update is being done via a normal save! and I'm not reloading the object.

--- UPDATE ---

I hadn't considered this when I posted the question, but my model uses the awesome_nested_set gem, and it appears that this is reloading the object or somehow interfering with the after_save callback. When I comment out acts_as_nested_set, the callback appears to be working fine.

--- UPDATE 2 ---

Fixed this with an around_save callback, which first determines if the attribute changed, then yields, then does the stuff I need it to do after the change has been made in the DB. The working solution looks like this:

around_save do |record, block|
  is_complete_changed = true if record.is_complete_changed?
  block.call
  if is_complete_changed
    ** do stuff **
  end
end
J Plato
  • 888
  • 1
  • 8
  • 17

2 Answers2

6

According to ActiveModel::Dirty source code

From line 274

 def changes_applied # :doc:
    @previously_changed = changes
    @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
 end

So the changes will be set to @previously_changed after changes_applied was called, and changes_apply was call when save was called, that means AFTER DOING PERSISTENT WORK (line 42)

In summary, previous_changes only has values when the record was actually saved to persistent storage (DB)

So in your callback, you may use record.changed_attributes, and outside use previously_changed, it will work fine!

Hieu Pham
  • 6,577
  • 2
  • 30
  • 50
1

I did not dig super deep, but from the first sight into ActiveModel::Dirty you can see, that in method previous_changes:

def previous_changes
  @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
end

@previously_changed is not defined anywhere (except for here, which uses the changes method I speak of below), thus you get the empty (nice and with indifferent access though :D) hash all the time.

What you really want to use, is a changes method:

def changes
  ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
end

It would return your expected

#=> {"is_complete"=>[true, false]}
Andrey Deineko
  • 51,333
  • 10
  • 112
  • 145
  • 1
    apparently, the awesome_nested_set gem is interfering with the callback; presumably by somehow reloading the object before the callback is fired (see my update above). not sure if there's a way to work around this. thanks for your help. -john – J Plato Feb 01 '16 at 22:06
  • permanent link to `changes` method: https://github.com/rails/rails/blob/2b584f165bc919c5e698e52d84c582a2f5f22b35/activemodel/lib/active_model/dirty.rb#L152 – dmkc Oct 21 '16 at 17:44