7
votes

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
2

2 Answers

5
votes

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!

1
votes

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]}