RoR 4.x 管理交易最佳实践的方法

RoR 4.x approach for managing transactions best practices

首先 - 很抱歉提出愚蠢的问题,但我是 RoR 世界的初学者,我需要基本了解数据库事务如何在模型/控制器中工作。我来自 Java 世界,在这个世界里,某些事情以某种方式完成,在使用 RoR 时,我自然会将它与 Java 网络框架进行比较,因此如果某些东西看起来完全不同,我会感到困惑:-)

在我的控制器操作之一中,我需要修改并保存多个模型,比方说:订单、发票、付款。

据我了解,每个模型的标准 "save" 方法在其自己的事务中执行,因此如果我简单地写:

payment.save
order.save
invoice.save

这将创建 3 个独立的数据库事务,并且每个模型都将保存在它自己的事务中——这不是我想要的,因为我想确保所有这些模型或 none已保存。

我找到了这篇文章:http://markdaggett.com/blog/2011/12/01/transactions-in-rails/,它演示了如何将多个 "saves" 包装到单个事务中。它已经很旧了,但我希望它仍然有效(如果我错了请纠正我)。

我不喜欢的一件事是我需要在我需要的每个控制器操作中显式管理这些事务。我希望这发生 "behind the scenes" 就像 "open session in view" 从 Java 世界知道的模式,其中数据库事务在执行任何数据库查询之前在过滤器中启动,并且事务在所有控制器之后在过滤器中提交动作已经完成。

我正在考虑在我的 RoR 应用程序中使用类似的方法,我发现博客 post 演示了如何做:http://blog.endpoint.com/2011/10/rails-controllers-and-transactions.html 但是我不确定这是否是 "best practice" 因为另一篇文章 (http://markdaggett.com/blog/2011/12/01/transactions-in-rails/) 说 "Use of transactions in a controller is common anti-pattern to avoid" (不知道为什么) .

有人可以指导我进入 "right" 方法吗?

谢谢,

迈克尔

一个基本的交易可以这样完成:

ActiveRecord::Base.transaction do
  payment.save!
  order.save!
  invoice.save!
end

然而,您需要的是一个服务对象,它将处理它。


class PaymentHadler
  def initialize(payment, order, invoice)
  end
  def perform
  end
end

根据您的应用,向 PaymentHandler 提供参数哈希并在处理程序中实例化模型可能是明智的。在大多数情况下,它们需要关联,因此内置在控制器内部。您的里程可能会有所不同:)

事务是模型层的关注点。我认为它们不属于控制器,尽管它肯定会起作用。这不是硬性规定。

需要考虑的一件事是此代码是否需要 运行 在控制器上下文之外?例如在后台工作?还是抽成任务?在那种情况下,将逻辑合并到模型层中会更容易重用。

要在模型层做到这一点,"Rails way"就是利用回调。 *_save/create/update 回调在当前数据库事务中自动执行,这意味着您无需显式编写事务代码即可获得所需的事务行为"for free"。

如果你的模型有明确的父子关系,那么你可以在"top level"模型上注册回调,例如:

class Order < ActiveRecord::Base
  has_one :invoice
  has_one :payment

  after_create :save_child_records

  private

  def save_child_records
    invoice.save!
    payment.save!
  end
end

order.save
# triggers invoice.save! and payment.save! 
# all in one transaction

或者,您可以创建一个不受数据库支持的全新模型 table,它仅用于编排多个模型。这有时称为 "service object" 或 "operation object"。

为此,我推荐 active_type gem,它允许您使用所有标准的 ActiveRecord 回调,因此提供相同的理想事务行为:

class ProcessPayment < ActiveType::Object
  attr_accessor :order, :invoice, :payment

  # You can declare ActiveRecord validations here too
  # Or register other before_save callbacks for business logic

  before_save :save_records

  private

  def save_records
    order.save!
    invoice.save!
    payment.save!
  end
end

process_payment.save
# order, invoice, payment are all saved in a single transaction