Сделать действия контроллера Rails атомарными? - PullRequest
1 голос
/ 15 февраля 2012

Иногда один из длинного ряда событий внутри действия контроллера завершается неудачей. Например, кредитная карта обрабатывается, но затем тайм-аут запроса ActiveRecord. Есть ли способ сделать эти звонки обратимыми?

например. с этим действием контроллера:

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, а затем мы должны вручную запустить остальные строки, которые должны были быть выполнены.

Ответы [ 3 ]

3 голосов
/ 15 февраля 2012

Вот общее решение.

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 голос
/ 19 сентября 2012

Я немного опоздал, но я думаю, что вы должны использовать save! вместо save.save просто возвращает false, если что-то не так в вашей модели, но сохранить!вызывает исключение, и ваш блок ActiveRecord::Base.transaction do правильно откатывает ваши изменения ...

Например:

def process_order
  ActiveRecord::Base.transaction do
    begin
      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
    rescue
      # Exception raised ... ROLLBACK
      raise ActiveRecord::Rollback
    end
end
1 голос
/ 15 февраля 2012

Просто оберните его в

cart.transaction do
  # ...
end

, чтобы использовать транзакции.Подробнее см. http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html

...