57

我有一个观察者,我注册了一个after_commit回调。 如何判断在创建或更新后是否被解雇? 我可以通过询问item.destroyed?来告诉某件物品被毁坏,但#new_record?自该物品被保存后不起作用。Rails 3:如何识别观察者中的after_commit动作? (创建/更新/销毁)

我正要加入after_create/after_update解决它,这样做@action = :create内,并检查@actionafter_commit,但似乎观察者实例是单身,我可能只覆盖一个值它到达之前after_commit。所以我以一种更丑陋的方式解决了这个问题,将action放在基于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

相反观察员我现在请使用ActiveSupport ::关注和after_commit :blah, on: :create那里工作。

+0

当after_commit被解雇时,你是否想知道你的记录是否是新的?重新阅读你的问题和答案,我觉得很困惑。你可以重述一下吗?或者给我们一个清晰的例子吗? – charlysisto

+0

如果您使用单线程服务器,则您的初始解决方案可以正常工作。如果你不使用它,然后切换到一个,如独角兽,这将以一种干净的方式解决这个问题。它使得编程模型变得容易得多,你将会减少头痛(像这样),最终它会更快。使用+ transaction_include_action?+不是干净的,因为它是一个不受rails测试套件中任何测试支持的不受支持的受保护的rails方法。下一个版本可能没有这种方法。 –

+0

@elado我很困惑。接受的答案(tardate's)不适用于观察者(正如ches的评论所指出的那样)。你是否转而使用回调呢?请附上对您的问题的解释。 – Kelvin

回答

50

我认为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或:destroy。

更新为Rails 4

对于那些寻求解决轨道4,5的问题,这种方法现在已经过时,你应该使用transaction_include_any_action?它接受的动作的array

使用例:

transaction_include_any_action?([:create]) 
+2

太棒了。这是我见过的最干净的方法。在其他情况下,我使用了@charlysisto提供的解决方案(它可以工作),但是这样感觉更好。我会试试这个。 –

+0

应该注意的是,这两种方法都受到保护,所以如果您想在观察者中调用它们,则必须使用所讨论的方法在模型对象上调用':send'。 – nirvdrum

+3

不要在rails的新版本中使用这个,使用下面的方法: 你可以使用'after_commit:method_name,on :: create' –

-4

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

id_changed? 

...在观察员中的助手。这在创建时是成立的,在更新时是错误的。

+2

'after_create'确实如此,'after_commit'则是false(对于创建和更新)。 – elado

+0

已更新。使用after_save事件来捕获创建/更新并能够区分这两者。 – Winfield

+1

但是'after_save'在事务中。我需要在它之外执行代码,因此,使用'after_commit'。 – elado

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) 
+0

谢谢,但它不起作用。 '#'当我做'after_commit(:on =>:create){| record |在观察者中放置“XXX”}'(它可能正在处理activerecord,但我在这里使用观察者)。 – elado

+2

它可以在更高版本的rails中使用。你可以这样做'after_commit:method_name,on::create' –

-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。

+0

这是行不通的,因为观察者的实例是一个单例。含义,所有记录共享@变量。如果多个记录由同一个观察者处理,则这些值将不正确。这就是为什么我创建了ID和动作的地图。 – elado

+0

是的我明白你在问题中的意思,我可以看到它。相应地更新了答案。你从你的错误中学习... – charlysisto

7

可以通过使用两种技术解决。

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

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

    • 在模型中before_createbefore_update callbacks基于leenasn的想法添加一个字段代码attr_accessor newly_created
    • 更新一样

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

,我创建了一些模块,可以使用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 
+0

为什么要这样做?此代码在我的应用程序中没有问题,我发现它非常有用... – lacco

53

我今天了解到,你可以做这样的事情:

after_commit :do_something, :on => :create 

after_commit :do_something, :on => :update 

do_something是要在某些行动调用回调方法。

如果你想呼吁更新相同的回调创建,但不破坏,你也可以使用: after_commit :do_something, :if => :persisted?

这真的不是记录很好,我的日子不好过谷歌搜索它。幸运的是,我认识一些有才华的人。希望能帮助到你!

+0

确认在rails上工作3.1.0 – sai

+2

这应该是公认的答案。 –

+6

这将不会与观察员一起工作,如问题描述中所述 –

0

我很想知道为什么你不能将after_commit逻辑移动到after_createafter_update。在后两个电话和after_commit之间是否发生了一些重要的状态变化?

如果您创建和更新操作有一些重叠的逻辑,你可以只是后者的2个方法调用第三个方法,传递动作:

# 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 
+2

性能和数据库锁定。如果我使用after_create/destroy,它会使包装事务处理时间延长,而我不需要事务处理。 – elado

-1

我用下面的代码,以确定它是否是一个新的记录或不:

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

它基于想法,一个新的记录具有默认的ID等于零,然后改变它保存。 当然id变化不是一个常见的情况,所以在大多数情况下,第二个条件可以省略。