将事务操作移离控制器

时间:2014-07-20 07:55:44

标签: ruby-on-rails-4 transactions rails-activerecord

  def cancel

    begin
      to_bank = @transfer.main_to_bank
      to_bank.with_lock do
        to_bank.locked_balance -= @transfer.amount
        to_bank.available_balance += @transfer.amount
        to_bank.save!
        @transfer.cancel
        @transfer.save!
      end
    rescue ActiveRecord::ActiveRecordError => e
      redirect_to admin_transfer_url(@transfer), alert: "Error while cancelling."
      return
    end

    redirect_to admin_transfer_url(@transfer), notice: 'Transfer was successfully cancelled.'
  end

我想将上面的代码重构为Transfer模型或其他地方,因为在其他地方使用了相同的代码。但是,ActiveRecord在模型中做了一些事务魔术,所以我担心通过简单地移动模型下的代码我可能会引入一些意想不到的副作用。

我的担心是否没有根据,上述代码通常会如何重构为控制器以外的可重用性?

更新:这似乎是服务对象的最佳位置,如http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/所述。

1 个答案:

答案 0 :(得分:2)

1)正如您在更新中提到的,这非常适合服务对象。将它们放在app / services之类的目录中,因为/ app中的任何内容都是自动加载的。有两种流行的方式来实现它们:

作为静态类:

AccountService.transfer(from_account, to_account, amount)

作为对象:

service = AccountService.new(from_account, to_account)
service.transfer(amount)

我更喜欢来自Java企业开发背景的选项,您可以使用类似的Java bean。

我还建议从所有服务中返回结果对象作为规则。这意味着您创建了一个名为“ServiceResult”的小类,其中包含一个布尔标志,表示调用是否成功,用户友好消息以及可选的结果对象(如果您没有服务结果对象,则返回值为方法)。换句话说,检查控制器或任何其他地方的结果将是:

response = AccountService.transfer(from, to, amount)

if response.success?
  flash[:notice] = response.message
else
  flash[:alert] = response.message
end

您始终可以将其重构为方法:

flash_service_result response

在为服务添加一些帮助方法之后,服务方法可能如下所示:

def self.transfer(from_account, to_account, amount)

   ActiveRecord::Base.transaction do
     ..do stuff..
     from_account.save!
     to_account.save!

     service_success("Transfer succesfull...")
   end

 rescue SomeException => error
   service_error("Failed to transfer...")

 rescue ActiveRecord::RecordInvalid => invalid_record_error
   service_validation_error(invalid_record_error)
 end

使用结果对象时,永远不要从您希望处理的服务中引发异常(在其他语言中检查异常)。

2)使用活动记录事务方法无论从何处调用,其行为都相同。它不会添加任何副作用。所以是的,你可以从控制器或服务中调用它。