Rails 3: How to define an after_commit action in watchers? (Create / Update / Destroy)

I have an observer and I am registering an after_commit . How can I find out if it was launched after creation or update? Can I say that the item was destroyed by requesting item.destroyed? but #new_record? does not work because the item was saved.

I was going to solve this problem by adding after_create / after_update and doing something like @action = :create inside and checking @action in after_commit , but it seems like the observer instance is a single, and I can just override the value before it reaches until after_commit . So I solved it in an ugly way by storing the action on the map based on item.id on after_create / update and checking its value on after_commit. Really ugly.

Is there another way?

Update

As @tardate, transaction_include_action? is a good indicator, although it is a private method, and in the observer it should be accessed using #send .

 class ProductScoreObserver < ActiveRecord::Observer observe :product def after_commit(product) if product.send(:transaction_include_action?, :destroy) ... 

Unfortunately, the :on option does not work in observers.

Just make sure you check the hell of your watchers (look for test_after_commit gem if you use use_transactional_fixtures), so when you upgrade to the new version of Rails, you will know if it still works.

(verified in 3.2.9)

Update 2

Instead of observers, I now use ActiveSupport :: Concern and after_commit :blah, on: :create works there.

+57
ruby-on-rails ruby-on-rails-3 observer-pattern transactions
Aug 30 '11 at 4:26 a.m.
source share
9 answers

I think transaction_include_action? - This is what you need. It provides reliable information about a specific transaction in the process (verified in version 3.0.8).

Formally, it determines whether the transaction included an action for: create ,: update, or: destroy. Used to filter callbacks.

 class Item < ActiveRecord::Base after_commit lambda { Rails.logger.info "transaction_include_action?(:create): #{transaction_include_action?(:create)}" Rails.logger.info "transaction_include_action?(:destroy): #{transaction_include_action?(:destroy)}" Rails.logger.info "transaction_include_action?(:update): #{transaction_include_action?(:update)}" } end 

Also interesting is transaction_record_state , which can be used to determine if a record was created or destroyed in a transaction. The state must be one of: new_record or: destroy.

Update for Rails 4

For those who want to solve the problem in Rails 4, this method is now deprecated, should you use transaction_include_any_action? which takes an array actions.

Usage example:

 transaction_include_any_action?([:create]) 
+50
Oct 03 2018-11-11T00:
source share

Today I found out that you can do something like this:

 after_commit :do_something, :on => :create after_commit :do_something, :on => :update 

Where do_something is the callback method that you want to call for specific actions.

If you want to call the same callback for update and create , but not destroy , you can also use: after_commit :do_something, :if => :persisted?

It really is not documented, and it was difficult for me to deal with it. Fortunately, I know some brilliant people. Hope this helps!

+53
Apr 27 '12 at 19:45
source share

You can solve using two methods.

  • The approach suggested by @nathanvda, i.e. checking created_at and updated_at. If they are the same, the record is created again, otherwise it is an update.

  • Using virtual attributes in the model. Steps:

    • Add a field to the model with the attr_accessor newly_created code
    • Update the same in before_create and before_update callbacks as

       def before_create (record) record.newly_created = true end def before_update (record) record.newly_created = false end 
+7
Oct 07 2018-11-11T00:
source share

Based on the idea of ​​leenasn, I created several modules that allow you to use the after_commit_on_update and after_commit_on_create : https://gist.github.com/2392664

Using:

 class User < ActiveRecord::Base include AfterCommitCallbacks after_commit_on_create :foo def foo puts "foo" end end class UserObserver < ActiveRecord::Observer def after_commit_on_create(user) puts "foo" end end 
+3
Apr 15 2018-12-12T00:
source share

Take a look at the test code: https://github.com/rails/rails/blob/master/activerecord/test/cases/transaction_callbacks_test.rb

Here you can find:

 after_commit(:on => :create) after_commit(:on => :update) after_commit(:on => :destroy) 

and

 after_rollback(:on => :create) after_rollback(:on => :update) after_rollback(:on => :destroy) 
+2
Sep 04 '11 at 18:36
source share

I am curious to know why you couldn't move the after_commit logic to after_create and after_update . Is there any important state change that occurs between the last 2 calls and after_commit ?

If your create and update processing has some overlapping logic, you can simply use the last methods 2 of the third method, passing the action:

 # Tip: on ruby 1.9 you can use __callee__ to get the current method name, so you don't have to hardcode :create and :update. class WidgetObserver < ActiveRecord::Observer def after_create(rec) # create-specific logic here... handler(rec, :create) # create-specific logic here... end def after_update(rec) # update-specific logic here... handler(rec, :update) # update-specific logic here... end private def handler(rec, action) # overlapping logic end end 

If you still prefer to use after_commit, you can use stream variables. This will not be a memory leak until dead threads can be garbage collected.

 class WidgetObserver < ActiveRecord::Observer def after_create(rec) warn "observer: after_create" Thread.current[:widget_observer_action] = :create end def after_update(rec) warn "observer: after_update" Thread.current[:widget_observer_action] = :update end # this is needed because after_commit also runs for destroy's. def after_destroy(rec) warn "observer: after_destroy" Thread.current[:widget_observer_action] = :destroy end def after_commit(rec) action = Thread.current[:widget_observer_action] warn "observer: after_commit: #{action}" ensure Thread.current[:widget_observer_action] = nil end # isn't strictly necessary, but it good practice to keep the variable in a proper state. def after_rollback(rec) Thread.current[:widget_observer_action] = nil end end 
0
Jul 05 2018-12-12T00:
source share

This is similar to your first approach, but it uses only one method (before_save or before_validate to really be safe), and I don't understand why this will override any value

 class ItemObserver def before_validation(item) # or before_save @new_record = item.new_record? end def after_commit(item) @new_record ? do_this : do_that end end 

Update

This solution does not work because, as pointed out by @eleano, ItemObserver is Singleton, it has only one instance. Thus, if 2 elements are saved at the same time, then @new_record can take its value from item_1, and after_commit item_2 is launched. To overcome this problem, there must be an item.id check / match to "post-synchronize" two callback methods: hacking.

-one
Oct 03 2018-11-11T00:
source share

I use the following code to determine if this is a new entry or not:

 previous_changes[:id] && previous_changes[:id][0].nil? 

Based on the idea that the new record has a default id of nil, and then changes it to save. Of course, changing the identifier is not an ordinary case, so in most cases the second condition can be omitted.

-one
Nov 20
source share

You can change the event binding from after_commit to after_save to capture all creation and update events. Then you can use:

 id_changed? 

... assistant to the observer. This will be true when created and false when updated.

-four
Aug 30 '11 at 4:32
source share



All Articles