after_commit for an attribute

Ruby on-RailsRuby

Ruby on-Rails Problem Overview


I am using an after_commit in my application.

I would like it to trigger only when a particular field is updated in my model. Anyone know how to do that?

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

Old question, but this is one method that I've found that might work with the after_commit callback (working off paukul's answer). At least, the values both persist post-commit in IRB.

after_commit :callback, 
  if: proc { |record| 
    record.previous_changes.key?(:attribute) &&
      record.previous_changes[:attribute].first != record.previous_changes[:attribute].last
  }

Solution 2 - Ruby on-Rails

Answering this old question because it still pops up in search results

you can use the [previous_changes][1] method which returnes a hash of the format:

{ "changed_attribute" => ["old value", "new value"] }

it's what [changes][2] was until the record gets actually saved (from active_record/attribute_methods/dirty.rb):

  def save(*) #:nodoc:
    if status = super
      @previously_changed = changes
      @changed_attributes.clear
      # .... whatever goes here

so in your case you can check for previous_changes.key? "your_attribute" or something like that

[1]: http://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-previous_changes "previous_changes" [2]: http://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changes "changes"

Solution 3 - Ruby on-Rails

Old question but still pops up in search results.

As of Rails 5 attribute_changed? was deprecated. Using saved_change_to_attribute? instead of attribute_changed? is recommended.

Solution 4 - Ruby on-Rails

I don't think you can do it in after_commit

The after_commit is called after the transaction is commited Rails Transactions

For example in my rails console

> record = MyModel.find(1)
=> #<MyModel id: 1, label: "test", created_at: "2011-08-19 22:57:54", updated_at: "2011-08-19 22:57:54">
> record.label = "Changing text"
=> "Changing text"
> record.label_changed?
=> true
> record.save
=> true
> record.label_changed?
=> false 

Therefore you won't be able to use the :if condition on after_commit because the attribute will not be marked as changed anymore as it has been saved. You may need to track whether the field you are after is changed? in another callback before the record is saved?

Solution 5 - Ruby on-Rails

This is a very old problem, but the accepted previous_changes solution just isn't robust enough. In an ActiveRecord transaction, there are many reasons why you might save a Model twice. previous_changes only reflects the result of the final save. Consider this example

class Test < ActiveRecord::Base
  after_commit: :after_commit_test

  def :after_commit_test
    puts previous_changes.inspect
  end
end

test = Test.create(number: 1, title: "1")
test = Test.find(test.id) # to initialize a fresh object

test.transaction do
  test.update(number: 2)
  test.update(title: "2")
end

which outputs:

{"title"=>["1", "2"], "updated_at"=>[...]}

but, what you need is:

{"title"=>["1", "2"], "number"=>[1, 2], "updated_at"=>[...]}

So, my solution is this:

module TrackSavedChanges
  extend ActiveSupport::Concern

  included do
    # expose the details if consumer wants to do more
    attr_reader :saved_changes_history, :saved_changes_unfiltered
    after_initialize :reset_saved_changes
    after_save :track_saved_changes
  end

  # on initalize, but useful for fine grain control
  def reset_saved_changes
    @saved_changes_unfiltered = {}
    @saved_changes_history = []
  end

  # filter out any changes that result in the original value
  def saved_changes
    @saved_changes_unfiltered.reject { |k,v| v[0] == v[1] }
  end

  private

  # on save
  def track_saved_changes
    # maintain an array of ActiveModel::Dirty.changes
    @saved_changes_history << changes.dup
    # accumulate the most recent changes
    @saved_changes_history.last.each_pair { |k, v| track_saved_change k, v }
  end

  # v is an an array of [prev, current]
  def track_saved_change(k, v)
    if @saved_changes_unfiltered.key? k
      @saved_changes_unfiltered[k][1] = track_saved_value v[1]
    else
      @saved_changes_unfiltered[k] = v.dup
    end
  end

  # type safe dup inspred by http://stackoverflow.com/a/20955038
  def track_saved_value(v)
    begin
      v.dup
    rescue TypeError
      v
    end
  end
end

which you can try out here: https://github.com/ccmcbeck/after-commit

Solution 6 - Ruby on-Rails

It sounds like you want something like a conditional callback. If you had posted some code I could have pointed you in the right direction however I think you would want to use something like this:

after_commit :callback,
  :if => Proc.new { |record| record.field_modified? }

Solution 7 - Ruby on-Rails

Use gem ArTransactionChanges. previous_changes is not working for me in Rails 4.0.x

Usage:

class User < ActiveRecord::Base
  include ArTransactionChanges

  after_commit :print_transaction_changes

  def print_transaction_changes
    transaction_changed_attributes.each do |name, old_value|
      puts "attribute #{name}: #{old_value.inspect} -> #{send(name).inspect}"
    end
  end
end

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionalikView Question on Stackoverflow
Solution 1 - Ruby on-Railsd_ethierView Answer on Stackoverflow
Solution 2 - Ruby on-RailsPascalView Answer on Stackoverflow
Solution 3 - Ruby on-Railssledge_909View Answer on Stackoverflow
Solution 4 - Ruby on-RailsPaul.sView Answer on Stackoverflow
Solution 5 - Ruby on-RailsChris BeckView Answer on Stackoverflow
Solution 6 - Ruby on-RailsDevin MView Answer on Stackoverflow
Solution 7 - Ruby on-RailsSimon LiuView Answer on Stackoverflow