after_commit for an attribute
Asked Answered
I

7

60

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?

Insufflate answered 19/8, 2011 at 22:31 Comment(0)
A
77

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
  }
Abdias answered 27/2, 2013 at 22:9 Comment(6)
Based on documentation, I think keys must be called: record.previous_changes.keys.include?(:attribute). With this change, it works for me.Rueful
record.previous_changes.key?(:attribute) is sufficient condition, because first and last values are always not equal.Remission
@jamesdevar is right, but this solution can fail if your model is saved more than once during the transaction. Here is a sample Rails project github.com/ccmcbeck/after-commit to demonstrate the problem and the my solution to fix it.Wheaten
I'm using rails 4.2.7.1. field_modified? did not work, this one works for me. Save my life!!!Vaucluse
For the one that want to check if the record is changed on more than one attribute: (record.previous_changes.keys & [:list, :of, :attributes]).any?Highbinder
Don't do what I did and accept this answer without first reading Chris Beck's answer further down! previous_changes can't be trusted in after_commit and can cause you a lot of trouble if you think it can!Shackelford
A
24

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

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

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

it's what changes 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

Ample answered 6/2, 2013 at 15:29 Comment(0)
B
11

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.

Berliner answered 11/12, 2018 at 10:7 Comment(0)
G
8

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?

Gretel answered 19/8, 2011 at 23:2 Comment(0)
W
8

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

Wheaten answered 30/5, 2016 at 16:25 Comment(1)
+1 for acknowledging this problem, I've run into it multiple times already. I'll take a sniff at your solution to see how well it'll fit my case!Plafker
U
4

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? }
Ulrika answered 19/8, 2011 at 22:45 Comment(1)
is field_modified? a pre-made method or is this a method i'll need to write to check the field value? basically i am trying to recalculate and save a user's rating based on all his appointment ratings. a rating to an appointment may be added after it has been created, so i'd like to know if the rating field was updated here is some of the code i am working with class Appointment < ActiveRecord::Base ... after_commit :update_user_and_product_ratings def update_user_and_product_ratings self.user.update_rating self.product.update_rating endInsufflate
O
0

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
Oliveira answered 7/5, 2015 at 7:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.