在不触发 Rails 的 AR 生命周期回调的情况下保存(恢复已删除的)AR 对象?

Save (restore deleted) AR object without triggering Rails' AR lifecycle callbacks?

提问人:Dan S. 提问时间:9/14/2023 最后编辑:Dan S. 更新时间:9/18/2023 访问量:43

问:

虽然这个问题是由 PaperTrail gem 引起的,但它也适用于一般的 Rails 的 ActiveRecord。

TL;博士:

如何在不触发 ActiveRecord 生命周期回调的情况下保存/创建新的 ActiveRecord 对象?

NTL;R:

当我从PaperTrail::Version备份中重新定义已删除的对象时,它实质上是初始化一个新的AR对象,并为其分配版本数据中的属性值。

然后,当我想要在数据库中恢复(持久化)该对象时,Rails 会运行它在创建新记录时会运行的所有常规回调,特别是 .如果任何回调更改了对象的状态,我们最终会得到一个对象,该对象与 Version 中的对象不同。after_create

对于一个非常简化的示例(这些回调中还发生了许多其他事情),有一个带有序列化字段的模型。对记录状态的每次更改都记录在该字段中:Payoutprotocol

  • after_create添加“%{timestamp} - 已请求付款”
  • before_update添加“%{timestamp} - 付款状态从'已请求'更改为'正在处理'”
  • 另一个添加“%{timestamp} - 付款状态从'处理中'更改为'完成'”before_update
  • before_destroy最后添加“%{timestamp} - 用户'whodunnit'删除的付款”

后来,当我从 Version 恢复记录时,它包含所有条目,然后在协议末尾再添加一个条目,说“%{timestamp} - Payout requested”。Payoutprotocol

...你可以想象当会计师看到这个时 WTF 的水平......

现在,缓解这种情况的方法之一是在回调中添加一个实例变量标志和/或一堆检查,等等......但是,当您需要在几十个模型中执行此操作时,它很快就会变得一团糟。

因此,我的问题是:如何在触发 ActiveRecord 生命周期回调的情况下,以与创建 PaperTrail 版本时完全相同的状态保存/恢复 PaperTrail 中的记录?(并且不会过多地入侵 Rails 的内部结构)。理想情况下,这将是一个通过一行代码包含在所有必要模型中的问题,因此我正在寻找一个通用的解决方案。

UPDATE: 需要功能来保存关联的嵌套记录,因此“update_columns”和原始值的普通插入实际上不是一个选项。作为最后的手段 - 也许,但前提是绝对没有其他办法。save

Ruby-on-Rails Rails-ActiveRecord Paper-Trail-gem

评论

2赞 dbugger 9/14/2023
stackoverflow.com/questions/632742/......
0赞 Dan S. 9/18/2023
差一点。那里提到的解决方案要么是向回调添加额外的标志,要么是使用“update_attributes”。正如我在问题中已经提到的,前者不是一种选择;后者并不真正合适,因为关联的记录没有被保存,就像它们在使用 .save

答:

0赞 mechnicov 9/18/2023 #1

为什么不使用原始 SQL 来插入恢复的数据

假设一些 Payout with 被删除payout_id

# Somehow find needed version
version =
  PaperTrail::Version.where(item_type: "Payout", item_id: payout_id).last

# Instantiate new Payout with all attributes of deleted one
deleted_payout = version.reify

# Use single SQL query without callbacks and instantiating model
Payout.insert(deleted_payout.attributes)

也可以跳过甚至重置回调,在这种情况下,使用通常的回调而不是插入save

Payout.skip_callback(:create, :after, :save_protocol_callback_name)
deleted_payout.save

这并不完全是问题的答案,但通常使用回调可能不是一个好主意

Rails 回调似乎是一个很酷的功能,直到它变成意大利面条,特别是当你对不同的业务操作有不同的流程时(例如,管理员、版主和用户的记录更新可能不同)

为了避免这种情况,值得使用一些包装器来执行类似于回调的操作。它可以是一些宝石,如 dry-rb 或 trailblazer,也可以是 PORO

例如(伪代码)

class UpdatePayout
  def self.call(payout_id, **attributes)
    new(payout_id, **attributes).call
  end

  def initialize(payout_id, **attributes)
    @payout = Payout.find(payout_id)
    @attributes = attributes
  end

  def call
    @payout.with_lock do
      old_status = @payout.status
      new_status = @attributes[:status]
      protocol = @payout.protocol

      if old_status != new_status
        protocol << "#{Time.now} status changed from #{old_status} to #{new_status}"
      end

      @protocol.update!(@attributes, protocol:)
    end
  end
end

在这种情况下,您不会用回调污染模型,并在需要时运行操作

尝试从papertrail中恢复数据时也不会有问题

评论

0赞 Dan S. 9/18/2023
save需要功能来保存关联的嵌套记录,因此“update_columns”和原始值的普通插入实际上不是一个选项。作为最后的手段 - 也许,但前提是绝对没有其他办法。