[ruby-on-rails] 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)?

The answer is


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


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.


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

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

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'].


Examples related to ruby-on-rails

Embed ruby within URL : Middleman Blog Titlecase all entries into a form_for text field Where do I put a single filter that filters methods in two controllers in Rails Empty brackets '[]' appearing when using .where How to integrate Dart into a Rails app Rails 2.3.4 Persisting Model on Validation Failure How to fix "Your Ruby version is 2.3.0, but your Gemfile specified 2.2.5" while server starting Is the server running on host "localhost" (::1) and accepting TCP/IP connections on port 5432? Rails: Can't verify CSRF token authenticity when making a POST request Uncaught ReferenceError: React is not defined

Examples related to ruby-on-rails-4

Uninitialized Constant MessagesController Is the server running on host "localhost" (::1) and accepting TCP/IP connections on port 5432? NameError: uninitialized constant (rails) How to solve error "Missing `secret_key_base` for 'production' environment" (Rails 4.1) ActionController::UnknownFormat Add a reference column migration in Rails 4 PG::ConnectionBad - could not connect to server: Connection refused Auto-loading lib files in Rails 4 cannot load such file -- bundler/setup (LoadError) Rails 4: how to use $(document).ready() with turbo-links

Examples related to model

Eloquent ORM laravel 5 Get Array of ids Tensorflow: how to save/restore a model? Cannot overwrite model once compiled Mongoose AngularJS - Binding radio buttons to models with boolean values Accessing MVC's model property from Javascript MVC 4 - how do I pass model data to a partial view? How to load json into my angular.js ng-model? Check if an object exists CodeIgniter PHP Model Access "Unable to locate the model you have specified" Removing a model in rails (reverse of "rails g model Title...")

Examples related to callback

When to use React setState callback How to send an HTTP request with a header parameter? javascript function wait until another function to finish What is the purpose of willSet and didSet in Swift? How to refactor Node.js code that uses fs.readFileSync() into using fs.readFile()? Aren't promises just callbacks? How do I convert an existing callback API to promises? How to access the correct `this` inside a callback? nodeJs callbacks simple example Callback after all asynchronous forEach callbacks are completed

Examples related to observer-pattern

Delegation: EventEmitter or Observable in Angular When should we use Observer and Observable? Determine what attributes were changed in Rails after_save callback? Super-simple example of C# observer/observable with delegates