提问人:Dan S. 提问时间:9/14/2023 最后编辑:Dan S. 更新时间:9/18/2023 访问量:43
在不触发 Rails 的 AR 生命周期回调的情况下保存(恢复已删除的)AR 对象?
Save (restore deleted) AR object without triggering Rails' AR lifecycle callbacks?
问:
虽然这个问题是由 PaperTrail gem 引起的,但它也适用于一般的 Rails 的 ActiveRecord。
TL;博士:
如何在不触发 ActiveRecord 生命周期回调的情况下保存/创建新的 ActiveRecord 对象?
NTL;R:
当我从PaperTrail::Version备份中重新定义已删除的对象时,它实质上是初始化一个新的AR对象,并为其分配版本数据中的属性值。
然后,当我想要在数据库中恢复(持久化)该对象时,Rails 会运行它在创建新记录时会运行的所有常规回调,特别是 .如果任何回调更改了对象的状态,我们最终会得到一个对象,该对象与 Version 中的对象不同。after_create
对于一个非常简化的示例(这些回调中还发生了许多其他事情),有一个带有序列化字段的模型。对记录状态的每次更改都记录在该字段中:Payout
protocol
after_create
添加“%{timestamp} - 已请求付款”before_update
添加“%{timestamp} - 付款状态从'已请求'更改为'正在处理'”- 另一个添加“%{timestamp} - 付款状态从'处理中'更改为'完成'”
before_update
before_destroy
最后添加“%{timestamp} - 用户'whodunnit'删除的付款”
后来,当我从 Version 恢复记录时,它包含所有条目,然后在协议末尾再添加一个条目,说“%{timestamp} - Payout requested”。Payout
protocol
...你可以想象当会计师看到这个时 WTF 的水平......
现在,缓解这种情况的方法之一是在回调中添加一个实例变量标志和/或一堆检查,等等......但是,当您需要在几十个模型中执行此操作时,它很快就会变得一团糟。
因此,我的问题是:如何在不触发 ActiveRecord 生命周期回调的情况下,以与创建 PaperTrail 版本时完全相同的状态保存/恢复 PaperTrail 中的记录?(并且不会过多地入侵 Rails 的内部结构)。理想情况下,这将是一个通过一行代码包含在所有必要模型中的问题,因此我正在寻找一个通用的解决方案。
UPDATE: 需要功能来保存关联的嵌套记录,因此“update_columns”和原始值的普通插入实际上不是一个选项。作为最后的手段 - 也许,但前提是绝对没有其他办法。save
答:
为什么不使用原始 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中恢复数据时也不会有问题
评论
save
需要功能来保存关联的嵌套记录,因此“update_columns”和原始值的普通插入实际上不是一个选项。作为最后的手段 - 也许,但前提是绝对没有其他办法。
评论
save