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

Posted

技术标签:

【中文标题】Rails 3:如何识别观察者中的 after_commit 动作? (创建/更新/销毁)【英文标题】:Rails 3: How to identify after_commit action in observers? (create/update/destroy) 【发布时间】:2011-11-06 12:16:13 【问题描述】:

我有一个观察者,我注册了一个after_commit 回调。 我如何判断它是否在创建或更新后被触发? 我可以通过询问 item.destroyed? 来判断某个项目已被销毁,但 #new_record? 无法正常工作,因为该项目已保存。

我打算通过添加after_create/after_update 并在里面执行类似@action = :create 并检查after_commit 处的@action 来解决它,但似乎观察者实例是一个单例,我可能只需在它到达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

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

【问题讨论】:

当 after_commit 被触发时,您是否想知道您的记录是否是新的?重新阅读您的问题和答案,我觉得很困惑。你能改写一下或者给我们一个明确的例子吗? 如果您使用单线程服务器,您的初始解决方案确实有效。如果您不使用一个,则切换到一个,例如独角兽,这将以干净的方式解决此问题。它使编程模型变得如此简单,您将不再那么头疼(就像这个一样),并且最终速度更快。使用 +transaction_include_action?+ 并不干净,因为它是不受支持的受保护的 rails 方法,不受 rails 测试套件中的任何测试的支持。下一个版本可能没有那个方法。 @elado 我很困惑。接受的答案(迟到的)不适用于观察者(如 ches 的评论所述)。你改用回调了吗?请对您的问题进行解释。 @Kelvin,请参阅我关于如何使其与 Observers 一起工作的问题更新。 【参考方案1】:

我认为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])

【讨论】:

太棒了。这是我见过的最干净的方法。在其他情况下,我使用了@charlysisto 提出的解决方案(确实有效),但这感觉更好。我会试试这个。 不要在新版本的rails上使用这个,使用以下方法:你可以这样做after_commit :method_name, on: :create @chris-finne ,@preston-marshall 是的,你是对的。如果您在 3.1/3.2 上,那么 after_commit :do_something, :on =&gt; :create 是要走的路。但在 3.0 中,情况有所不同。 “面向未来的铁轨”有点矛盾! 最初的问题是指观察者。你仍然不能在观察者中使用:on =&gt; :create,否则这是一种非常干净的方式来处理模型类不负责的事情,比如发送通知。必须使用回调会破坏将此类逻辑排除在模型之外的目的。 +1。很高兴知道,但我同意它不适用于观察者。【参考方案2】:

我今天了解到你可以这样做:

after_commit :do_something, :on => :create

after_commit :do_something, :on => :update

其中 do_something 是您要在某些操作上调用的回调方法。

如果您想为 updatecreate 调用相同的回调,而不是 destroy,您还可以使用: after_commit :do_something, :if =&gt; :persisted?

确实没有很好的记录,我很难用谷歌搜索它。幸运的是,我认识一些才华横溢的人。希望对您有所帮助!

【讨论】:

这不适用于观察者,如问题描述中所述 @SujoyGupta - 不应该,因为问题是关于观察者的。【参考方案3】:

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

@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

【讨论】:

【参考方案4】:

基于 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

【讨论】:

为什么要对此投反对票?这段代码在我的应用程序中运行没有问题,我发现它非常有用......【参考方案5】:

看一下测试代码: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)

【讨论】:

谢谢,但它不起作用。 #&lt;NoMethodError: undefined method 'after_commit' for ...&gt; 当我在观察者中执行 after_commit(:on =&gt; :create)|record| puts "XXX" 时(它可能正在处理活动记录,但我在这里使用观察者)。 它确实适用于更高版本的 rails。你可以做after_commit :method_name, on: :create【参考方案6】:

我用下面的代码判断是否是新记录:

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

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

【讨论】:

【参考方案7】:

我很想知道为什么你不能将你的 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

【讨论】:

性能和数据库锁定。如果我使用 after_create/destroy 它会使包装事务更长,而我不需要事务。【参考方案8】:

这类似于您的第一种方法,但它只使用一种方法(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 是一个单例,它只有一个实例。因此,如果同时保存 2 个项目,@new_record 可以从 item_1 获取其值,而 after_commit 由 item_2 触发。为了克服这个问题,应该有一个item.id 检查/映射到“同步后”两个回调方法:hackish。

【讨论】:

这行不通,因为观察者的实例是单例。意思是,@ 变量为所有记录共享。如果多个记录由同一个观察者处理,则值将不正确。这就是我创建 ID 和操作地图的原因。 是的,我明白你在这个问题中的意思,因为我可以看到它。相应地更新了答案。你从错误中学习......【参考方案9】:

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

id_changed?

...观察者的助手。这将在创建时为真,在更新时为假。

【讨论】:

after_create 中为真,after_commit 中为假(用于创建和更新)。 已更新。使用 after_save 事件来捕获创建/更新并能够区分两者。 但是after_save 在事务内部。我需要在它之外执行代码,因此使用after_commit

以上是关于Rails 3:如何识别观察者中的 after_commit 动作? (创建/更新/销毁)的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Rails 5 上的 ruby​​ 中添加人脸识别?

Rails 3 和 Paperclip:“'identify' 命令无法识别”

为啥 Rails 无法识别请求标头中的 `Accept: application/json`?

如何识别 Rails 6 批量插入错误

ruby 方法中如何使用符号来识别参数

Ruby On Rails 3.1 中的尾随日志文件