Rails 3:如何在观察者中识别after_commit动作?(创建/更新/破坏)

时间:2021-10-04 01:21:15

I have an observer and I register an after_commit callback. How can I tell whether it was fired after create or update? I can tell an item was destroyed by asking item.destroyed? but #new_record? doesn't work since the item was saved.

我有一个观察者,我注册了一个after_commit回调。我如何知道它是在创建或更新后被触发的?我可以通过询问物品来判断物品是否被销毁?但是# new_record呢?由于保存了项目,所以无法工作。

I was going to solve it by adding after_create/after_update and do something like @action = :create inside and check the @action at after_commit, but it seems that the observer instance is a singleton and I might just override a value before it gets to the after_commit. So I solved it in an uglier way, storing the action in a map based on the item.id on after_create/update and checking its value on after_commit. Really ugly.

我将通过添加after_create/after_update来解决这个问题,并做一些类似于@action =:在after_commit中创建并检查@action,但是看起来观察者实例是单例的,我可能只是在它到达after_commit之前重写一个值。所以我以一种更丑陋的方式解决了这个问题,将这个动作存储在一个基于这个项目的地图中。在after_create/update上的id,并在after_commit上检查其值。真的很丑。

Is there any other way?

还有别的办法吗?

Update

As @tardate said, transaction_include_action? is a good indication, though it's a private method, and in an observer it should be accessed with #send.

@tardate说过,transaction_include_action吗?这是一个很好的指示,尽管它是一个私有方法,并且在观察者中应该使用#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.

不幸的是,on选项在观察者中不起作用。

Just make sure you test the hell of your observers (look for test_after_commit gem if you use use_transactional_fixtures) so when you upgrade to new Rails version you'll know if it still works.

只要确保您测试了大量的观察者(如果您使用use_transactional_fixtures的话,请查找test_after_commit gem),这样当您升级到新的Rails版本时,您就会知道它是否仍然有效。

(Tested on 3.2.9)

3.2.9(测试)

Update 2

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

我现在使用ActiveSupport::Concern和after_commit:blah, on: create工作。

9 个解决方案

#1


50  

I think transaction_include_action? is what you are after. It gives a reliable indication of the specific transaction in process (verified in 3.0.8).

我认为transaction_include_action吗?是你所追求的。它给出了正在处理的特定事务的可靠指示(在3.0.8中验证)。

Formally, it determines if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.

在形式上,它确定事务是否包含用于:create、:update或:destroy的操作。用于过滤回调。

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 of interest may be transaction_record_state which can be used to determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed.

同样值得关注的还有transaction_record_state,它可以用来确定在事务中创建或销毁记录。状态应该是:new_record或:destroy。

Update for Rails 4

Rails更新4

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

对于那些想要解决Rails 4中的问题的人,这个方法现在已经被弃用了,您应该使用transaction_include_any_action?它接受一系列动作。

Usage Example:

使用的例子:

transaction_include_any_action?([:create])

#2


54  

I've learned today 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 you want to call on certain actions.

do_something是要调用某些操作的回调方法。

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?

如果您希望为更新和创建调用相同的回调,但不销毁,您还可以使用:after_commit:do_something,: If =>:persist ?

It's really not documented well and I had a hard time Googling it. Luckily, I know a few brilliant people. Hope it helps!

它没有被很好地记录下来,而且我很难在谷歌上搜索它。幸运的是,我认识一些聪明的人。希望它可以帮助!

#3


7  

You can solve by using two techniques.

你可以用两种方法来解决。

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

    @nathanvda建议的方法,即检查created_at和updated_at。如果它们是相同的,记录是新创建的,否则就是更新。

  • By using virtual attributes in the model. Steps are:

    通过在模型中使用虚拟属性。步骤是:

    • Add a field in the model with the code attr_accessor newly_created
    • 使用代码attr_accessor新创建的代码在模型中添加一个字段。
    • Update the same in the before_create and before_update callbacks as

      在before_create和before_update回调中更新相同的内容

      def before_create (record)
          record.newly_created = true
      end
      
      def before_update (record)
          record.newly_created = false
      end
      

#4


3  

Based on leenasn idea, I created some modules that makes it possible to use after_commit_on_updateand after_commit_on_create callbacks: https://gist.github.com/2392664

基于leenasn思想,我创建了一些模块,使使用after_commit_on_updateand after_commit_on_create回调成为可能:https://gist.github.com/2392664

Usage:

用法:

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

#5


2  

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

看看测试代码:https://github.com/rails/rails/blob/master/activerecord/test/cases/transaction_callbacks_test.rb

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

#6


0  

I'm curious to know why you couldn't move your after_commit logic into after_create and after_update. Is there some important state change that happens between the latter 2 calls and after_commit?

我很想知道为什么不能将after_commit逻辑移动到after_create和after_update中。后两个调用和after_commit之间是否发生了一些重要的状态更改?

If your create and update handling has some overlapping logic, you could just have the latter 2 methods call a third method, passing in 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 rather use after_commit, you can use thread variables. This won't leak memory as long as dead threads are allowed to be garbage-collected.

如果仍然使用after_commit,则可以使用线程变量。只要允许垃圾收集死线程,就不会泄漏内存。

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's good practice to keep the variable in a proper state.
  def after_rollback(rec)
    Thread.current[:widget_observer_action] = nil
  end
end

#7


-1  

This is similar to your 1st approach but it only uses one method (before_save or before_validate to really be safe) and I don't see why this would override any value

这类似于您的第一种方法,但它只使用一个方法(before_save或before_validate以确保安全性),我不明白为什么它会覆盖任何值

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 doesn't work because as stated by @eleano, ItemObserver is a Singleton, it has only one instance. So if 2 Item are saved at the same time @new_record could take its value from item_1 while after_commit is triggered by item_2. To overcome this problem there should be an item.id checking/mapping to "post-synchornize" the 2 callback methods : hackish.

这个解决方案不起作用,因为如@eleano所述,ItemObserver是一个单例,它只有一个实例。因此,如果同时保存2项,@new_record可以从item_1获取其值,而item_2触发after_commit。要解决这个问题,应该有一个项目。id检查/映射到“后synchornize”两个回调方法:hackish。

#8


-1  

I use the following code to determine whether it is a new record or not:

我使用以下代码来确定它是否是一个新的记录:

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

It based on idea that a new record has default id equal to nil and then changes it on save. Of course id changing is not a common case, so in most cases the second condition can be omitted.

它基于一个新记录的默认id为nil,然后在保存时更改它的想法。当然,id更改不是一种常见的情况,因此在大多数情况下可以省略第二个条件。

#9


-4  

You can change your event hook from after_commit to after_save, to capture all create and update events. You can then use:

您可以将事件挂钩从after_commit更改为after_save,以捕获所有创建和更新事件。然后,您可以使用:

id_changed?

...helper in the observer. This will be true on create and false on an update.

…辅助的观察者。这在创建时为真,在更新时为假。

#1


50  

I think transaction_include_action? is what you are after. It gives a reliable indication of the specific transaction in process (verified in 3.0.8).

我认为transaction_include_action吗?是你所追求的。它给出了正在处理的特定事务的可靠指示(在3.0.8中验证)。

Formally, it determines if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.

在形式上,它确定事务是否包含用于:create、:update或:destroy的操作。用于过滤回调。

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 of interest may be transaction_record_state which can be used to determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed.

同样值得关注的还有transaction_record_state,它可以用来确定在事务中创建或销毁记录。状态应该是:new_record或:destroy。

Update for Rails 4

Rails更新4

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

对于那些想要解决Rails 4中的问题的人,这个方法现在已经被弃用了,您应该使用transaction_include_any_action?它接受一系列动作。

Usage Example:

使用的例子:

transaction_include_any_action?([:create])

#2


54  

I've learned today 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 you want to call on certain actions.

do_something是要调用某些操作的回调方法。

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?

如果您希望为更新和创建调用相同的回调,但不销毁,您还可以使用:after_commit:do_something,: If =>:persist ?

It's really not documented well and I had a hard time Googling it. Luckily, I know a few brilliant people. Hope it helps!

它没有被很好地记录下来,而且我很难在谷歌上搜索它。幸运的是,我认识一些聪明的人。希望它可以帮助!

#3


7  

You can solve by using two techniques.

你可以用两种方法来解决。

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

    @nathanvda建议的方法,即检查created_at和updated_at。如果它们是相同的,记录是新创建的,否则就是更新。

  • By using virtual attributes in the model. Steps are:

    通过在模型中使用虚拟属性。步骤是:

    • Add a field in the model with the code attr_accessor newly_created
    • 使用代码attr_accessor新创建的代码在模型中添加一个字段。
    • Update the same in the before_create and before_update callbacks as

      在before_create和before_update回调中更新相同的内容

      def before_create (record)
          record.newly_created = true
      end
      
      def before_update (record)
          record.newly_created = false
      end
      

#4


3  

Based on leenasn idea, I created some modules that makes it possible to use after_commit_on_updateand after_commit_on_create callbacks: https://gist.github.com/2392664

基于leenasn思想,我创建了一些模块,使使用after_commit_on_updateand after_commit_on_create回调成为可能:https://gist.github.com/2392664

Usage:

用法:

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

#5


2  

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

看看测试代码:https://github.com/rails/rails/blob/master/activerecord/test/cases/transaction_callbacks_test.rb

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

#6


0  

I'm curious to know why you couldn't move your after_commit logic into after_create and after_update. Is there some important state change that happens between the latter 2 calls and after_commit?

我很想知道为什么不能将after_commit逻辑移动到after_create和after_update中。后两个调用和after_commit之间是否发生了一些重要的状态更改?

If your create and update handling has some overlapping logic, you could just have the latter 2 methods call a third method, passing in 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 rather use after_commit, you can use thread variables. This won't leak memory as long as dead threads are allowed to be garbage-collected.

如果仍然使用after_commit,则可以使用线程变量。只要允许垃圾收集死线程,就不会泄漏内存。

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's good practice to keep the variable in a proper state.
  def after_rollback(rec)
    Thread.current[:widget_observer_action] = nil
  end
end

#7


-1  

This is similar to your 1st approach but it only uses one method (before_save or before_validate to really be safe) and I don't see why this would override any value

这类似于您的第一种方法,但它只使用一个方法(before_save或before_validate以确保安全性),我不明白为什么它会覆盖任何值

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 doesn't work because as stated by @eleano, ItemObserver is a Singleton, it has only one instance. So if 2 Item are saved at the same time @new_record could take its value from item_1 while after_commit is triggered by item_2. To overcome this problem there should be an item.id checking/mapping to "post-synchornize" the 2 callback methods : hackish.

这个解决方案不起作用,因为如@eleano所述,ItemObserver是一个单例,它只有一个实例。因此,如果同时保存2项,@new_record可以从item_1获取其值,而item_2触发after_commit。要解决这个问题,应该有一个项目。id检查/映射到“后synchornize”两个回调方法:hackish。

#8


-1  

I use the following code to determine whether it is a new record or not:

我使用以下代码来确定它是否是一个新的记录:

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

It based on idea that a new record has default id equal to nil and then changes it on save. Of course id changing is not a common case, so in most cases the second condition can be omitted.

它基于一个新记录的默认id为nil,然后在保存时更改它的想法。当然,id更改不是一种常见的情况,因此在大多数情况下可以省略第二个条件。

#9


-4  

You can change your event hook from after_commit to after_save, to capture all create and update events. You can then use:

您可以将事件挂钩从after_commit更改为after_save,以捕获所有创建和更新事件。然后,您可以使用:

id_changed?

...helper in the observer. This will be true on create and false on an update.

…辅助的观察者。这在创建时为真,在更新时为假。