2012-02-14 19 views
1

コントローラー・アクション内の長い一連のイベントの1つが失敗することがあります。たとえば、クレジットカードが処理された後、ActiveRecordクエリがタイムアウトします。これらの通話を可逆にする方法はありますか?Railsコントローラのアクションをアトミックにしますか?

など。このコントローラのアクションで:

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 

これは単なる不自然な例であると私は本当にわからないんだけどそれはどのようにだろう実際に働く。典型的には、submit_orderの行に対してActiveRecord::StatementInvalid: : execution expiredのような例外が発生し、次に実行しなければならない残りの行を手動で実行する必要があります。

答えて

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 

注:ロールバック・ブロック

    • ハンドルエラーが失敗したタスクのロールバックを呼び出して、それは私が「
  • +0

    非常に良い。このソリューションを見ると、同僚はステートマシンを使用するように提案しました。これは特定の状況で使用できますが、これは一般的な解決策です。 –

    1

    だけでトランザクションを使用する

    cart.transaction do 
        # ... 
    end 
    

    でそれをラップします。詳細はhttp://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html

    +0

    私はActiveRecordのクエリのためにうまく動作することを確認しています。たぶん私は私の質問では、私はさまざまな種類のイベントで動作するソリューションを探している明確にされている必要があります。 1つはAPI呼び出しであり、別の1つはActiveRecordクエリであり、もう1つはMongoidクエリである。 –

    +0

    その後、Exceptionを 'レスキュー 'して変更をロールバックします。 – iblue

    1

    を調整するのは簡単です少し遅いですが、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 
    
    関連する問題