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

时间:2011-08-30 04:26:21

标签: ruby-on-rails ruby-on-rails-3 transactions observer-pattern

我有一个观察者,我注册了一个after_commit回调。 如何判断它是在创建或更新后被触发的? 我可以通过询问item.destroyed?告诉某个项目已被销毁,但#new_record?因项目已保存而无效。

我打算通过添加after_create / after_update并在@action = :create内部执行此操作来解决问题,并在@action检查after_commit,但似乎观察者实例是一个单例,我可能只是在它到达after_commit之前覆盖一个值。所以我以一种更丑陋的方式解决了这个问题,根据after_create / update上的item.id将动作存储在地图中,并在after_commit上检查它的值。真的很难看。

还有其他办法吗?

更新

正如@tardate所说,transaction_include_action?是一个很好的指示,虽然它是一种私有方法,但在观察者中它应该用#send来访问。

class ProductScoreObserver < ActiveRecord::Observer
  observe :product

  def after_commit(product)
    if product.send(:transaction_include_action?, :destroy)
      ...

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

请确保测试观察者的地狱(如果使用use_transactional_fixtures则查找test_after_commit gem),因此当您升级到新的Rails版本时,您将知道它是否仍然有效。

(在3.2.9上测试)

更新2

而不是Observers我现在使用ActiveSupport :: Concern,after_commit :blah, on: :create在那里工作。

9 个答案:

答案 0 :(得分:55)

我今天已经知道你可以做这样的事情:

after_commit :do_something, :on => :create

after_commit :do_something, :on => :update

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

如果您要为更新创建而不是销毁调用相同的回调,您还可以使用: after_commit :do_something, :if => :persisted?

它真的没有记录好,我很难用谷歌搜索它。幸运的是,我认识一些聪明的人。希望它有所帮助!

答案 1 :(得分:53)

我认为transaction_include_action?就是你所追求的。它给出了正在进行的特定交易的可靠指示(在3.0.8中验证)。

正式地,它确定事务是否包含以下操作: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

感兴趣的还有transaction_record_state,可用于确定是否在交易中创建或销毁了记录。州应该是以下之一:new_record或:destroyed。

Rails 4的更新

对于那些寻求在Rails 4中解决问题的人,现在不推荐使用此方法,您应该使用接受transaction_include_any_action?操作的array

用法示例:

transaction_include_any_action?([:create])

答案 2 :(得分:7)

您可以使用两种技术解决。

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

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

    • 使用代码attr_accessor newly_created
    • 在模型中添加字段
    • before_createbefore_update callbacks中更新

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

答案 3 :(得分:3)

基于leenasn的想法,我创建了一些模块,可以使用after_commit_on_updateafter_commit_on_create回调:https://gist.github.com/2392664

用法:

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

答案 4 :(得分:2)

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

在那里你可以找到:

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

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

答案 5 :(得分:0)

我很想知道为什么你无法将after_commit逻辑移到after_createafter_update。在后两次调用和after_commit之间是否会发生一些重要的状态变化?

如果您的创建和更新处理有一些重叠的逻辑,您可以让后两种方法调用第三种方法,传递操作:

# 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

如果您仍然使用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

答案 6 :(得分:0)

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

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

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

答案 7 :(得分:-1)

这类似于你的第一种方法,但它只使用一种方法(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

更新

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

答案 8 :(得分:-4)

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

id_changed?

...在观察者中帮助。这在创建时为true,在更新时为false。