Determine what attributes were changed in Rails after_save callback?
I'm setting up an after_save callback in my model observer to send a notification only if the model's published attribute was changed from false to true. Since methods such as changed? are only useful before the model is saved, the way I'm currently (and unsuccessfully) trying to do so is as follows:
def before_save(blog)
@og_published = blog.published?
end
def after_save(blog)
if @og_published == false and blog.published? == true
Notification.send(...)
end
end
Does anyone have any suggestions as to the best way to handle this, preferably using model observer callbacks (so as not to pollute my controller code)?
Rails 5.1+
Use saved_change_to_published?
:
class SomeModel < ActiveRecord::Base
after_update :send_notification_after_change
def send_notification_after_change
Notification.send(…) if (saved_change_to_published? && self.published == true)
end
end
Or if you prefer, saved_change_to_attribute?(:published)
.
Rails 3–5.1
Warning
This approach works through Rails 5.1 (but is deprecated in 5.1 and has breaking changes in 5.2). You can read about the change in this pull request.
In your after_update
filter on the model you can use _changed?
accessor. So for example:
class SomeModel < ActiveRecord::Base
after_update :send_notification_after_change
def send_notification_after_change
Notification.send(...) if (self.published_changed? && self.published == true)
end
end
It just works.
For those who want to know the changes just made in an after_save
callback:
Rails 5.1 and greater
model.saved_changes
Rails < 5.1
model.previous_changes
Also see: http://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-previous_changes
To anyone seeing this later on, as it currently (Aug. 2017) tops google: It is worth mentioning, that this behavior will be altered in Rails 5.2, and has deprecation warnings as of Rails 5.1, as ActiveModel::Dirty changed a bit.
What do I change?
If you're using attribute_changed?
method in the after_*
-callbacks, you'll see a warning like:
DEPRECATION WARNING: The behavior of
attribute_changed?
inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method aftersave
returned (e.g. the opposite of what it returns now). To maintain the current behavior, usesaved_change_to_attribute?
instead. (called from some_callback at /PATH_TO/app/models/user.rb:15)
As it mentions, you could fix this easily by replacing the function with saved_change_to_attribute?
. So for example, name_changed?
becomes saved_change_to_name?
.
Likewise, if you're using the attribute_change
to get the before-after values, this changes as well and throws the following:
DEPRECATION WARNING: The behavior of
attribute_change
inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method aftersave
returned (e.g. the opposite of what it returns now). To maintain the current behavior, usesaved_change_to_attribute
instead. (called from some_callback at /PATH_TO/app/models/user.rb:20)
Again, as it mentions, the method changes name to saved_change_to_attribute
which returns ["old", "new"]
.
or use saved_changes
, which returns all the changes, and these can be accessed as saved_changes['attribute']
.
In case you can do this on before_save
instead of after_save
, you'll be able to use this:
self.changed
it returns an array of all changed columns in this record.
you can also use:
self.changes
which returns a hash of columns that changed and before and after results as arrays
The "selected" answer didn't work for me. I'm using rails 3.1 with CouchRest::Model (based on Active Model). The _changed?
methods don't return true for changed attributes in the after_update
hook, only in the before_update
hook. I was able to get it to work using the (new?) around_update
hook:
class SomeModel < ActiveRecord::Base
around_update :send_notification_after_change
def send_notification_after_change
should_send_it = self.published_changed? && self.published == true
yield
Notification.send(...) if should_send_it
end
end