使Rails控制器动作原子化?

时间:2012-02-14 21:41:21

标签: ruby-on-rails exception-handling transactions rollback

有时控制器操作中的一系列事件中的一个失败。例如,处理信用卡但随后ActiveRecord查询超时。有没有办法让这些电话变得可逆?

E.g。使用此控制器操作:

def process_order
  cart = Cart.new(params[:cart])
  load_order
  response = credit_card.charge
  if response
    submit_order
    order.receipt = Pdf.new(render_to_string(:partial => 'receipt')
    order.receipt.pdf.generate
    order.receipt.save
    render :action => 'finished'
  else
    order.transaction = response
    @message = order.transaction.message
    order.transaction.save
    render :action => 'charge_failed'
  end
end

我希望能像这样放置一个块:

def process_order
  transaction
    cart = Cart.new(params[:cart])
    load_order
    response = credit_card.charge
    if response
      submit_order
      order.receipt = Pdf.new(render_to_string(:partial => 'receipt')
      order.receipt.pdf.generate
      order.receipt.save
      render :action => 'finished'
    else
      order.transaction = response
      @message = order.transaction.message
      order.transaction.save
      render :action => 'charge_failed'
    end
  rollback
    credit_card.cancel_charge
    ...
  end
end

这只是一个人为的例子,我不确定它在实践中会起作用。通常情况下,我们会为ActiveRecord::StatementInvalid: : execution expired的行获取submit_order之类的异常,然后我们必须手动运行应该运行的其余行。

2 个答案:

答案 0 :(得分:3)

这是一个通用解决方案。

class Transactable
  def initialize(&block)
    raise LocalJumpError unless block_given?
    @block = block
  end
  def on_rollback(&block)
    raise LocalJumpError unless block_given?
    @rollback = block
    self
  end
  def call
    @block.call
  end
  def rollback
    @rollback.call if @rollback
  end
end

class Transaction
  def initialize(tasks)
    tasks = Array(tasks)
    tasks.each do |t|
      Transactable === t or raise TypeError
    end
    @tasks = tasks
  end
  def run
    finished_tasks = []
    begin
      @tasks.each do |t|
        t.call
        finished_tasks << t
      end
    rescue => err
      finished_tasks.each do |t|
        t.rollback
      end
      raise err
    end
  end
end

if __FILE__ == $0
  Transaction.new([
    Transactable.new { puts "1: call" }.on_rollback { puts "1: rollback" },
    Transactable.new { puts "2: call" }.on_rollback { puts "2: rollback" },
    Transactable.new { puts "3: call"; raise "fail!" }.on_rollback { puts "3: rollback" },
  ]).run
end

请注意,它不会:

  • 处理回滚块中的错误
  • 调用失败任务的回滚,但这很容易调整

答案 1 :(得分:1)

将其包装在

cart.transaction do
  # ...
end

使用交易。有关详细信息,请参阅http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html