207

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)?

7 Answers 7

269

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.

12
  • Forget what I said above - it DOESN'T work in Rails 2.0.5. So a useful addition to Rails 3.
    – stephenr
    Oct 5, 2010 at 8:34
  • 4
    I think after_update is deprecated now? Anyway, I tried this in an after_save hook and that seemed to work fine. (The changes() hash still hasn't been reset yet in an after_save, apparently.)
    – Tyler Rick
    Mar 14, 2013 at 5:09
  • 3
    I had to include this line in model.rb file. include ActiveModel::Dirty Jan 7, 2016 at 5:50
  • 13
    In later versions of Rails you can add the condition to the after_update call: after_update :send_notification_after_change, if: -> { published_changed? }
    – Koen.
    Mar 30, 2016 at 20:31
  • 12
    There will be some API changes in Rails 5.2. You'll have to do saved_change_to_published? or saved_change_to_published to fetch the change during the callback
    – alopez02
    Apr 4, 2018 at 10:04
226

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

6
  • 3
    This works perfectly when you don't want to use model callbacks and need a valid save to happen before carrying out further functionality. Jan 20, 2015 at 3:39
  • 5
    Just to be clear: in my tests (Rails 4), if you are using an after_save callback, self.changed? is true and self.attribute_name_changed? is also true, but self.previous_changes returns an empty hash.
    – sandre89
    Nov 24, 2016 at 4:54
  • looks like previous_changes is a Rails 5 function. You can implement this in Rails 4 by adding an attr_accessor :previous_change, fill it in a before_save call and access it again in an after_save. Dec 8, 2016 at 15:09
  • @GernotUllrich Also works in Rails 4.2: devdocs.io/rails~4.2/activemodel/… Jan 5, 2017 at 16:59
  • 11
    This is deprecated in Rails 5.1 +. Use saved_changes in after_save callbacks instead
    – rico_mac
    Apr 26, 2017 at 19:03
87

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 after save returned (e.g. the opposite of what it returns now). To maintain the current behavior, use saved_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 after save returned (e.g. the opposite of what it returns now). To maintain the current behavior, use saved_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'].

1
  • 2
    Note this helpful response also includes workarounds for the deprecation of attribute_was methods: use saved_change_to_attribute instead. Jul 24, 2018 at 21:04
52

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

4
  • 9
    Except that these don't work in an after_ callback, which is what the question was actually about. @jacek-głodek's answer below is the correct one.
    – Jazz
    Dec 22, 2014 at 21:26
  • Updated the answer to make it clear this only applies to before_save
    – mahemoff
    Apr 4, 2015 at 13:00
  • 1
    Which Rails version is this? In Rails 4, self.changed can be used in after_save callbacks.
    – sandre89
    Nov 24, 2016 at 4:55
  • Works great! Also to be noted, the result of self.changed is an array of strings! (Not symbols!) ["attr_name", "other_attr_name"]
    – LukeS
    Mar 2, 2017 at 23:18
8

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
1
  • 1
    The selected answer also didn't work for me, but this did. Thanks! I'm using ActiveRecord 3.2.16.
    – Ben Lee
    Feb 12, 2014 at 2:47
7

you can add a condition to the after_update like so:

class SomeModel < ActiveRecord::Base
  after_update :send_notification, if: :published_changed?

  ...
end

there's no need to add a condition within the send_notification method itself.

-22

You just add an accessor who define what you change

class Post < AR::Base
  attr_reader :what_changed

  before_filter :what_changed?

  def what_changed?
    @what_changed = changes || []
  end

  after_filter :action_on_changes

  def action_on_changes
    @what_changed.each do |change|
      p change
    end
  end
end
1
  • 2
    This is a very bad practice, don't even consider it.
    – Nuno Silva
    May 17, 2018 at 12:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.