なぜTransactionが必要なのか
Transationの目的は、あるいコードブロックにあるSQL文の変更を、全部成功することを守るための存在である。Transactionにより、データの統一性を保ことができる。銀行などの受け入れと引き出しの処理には必要でしょう。二つの処理の中一つが失敗すると、コードブロークにあるSQL処理を全部ロールバックされるのが、Transactionの特徴である。
ActiveRecord::Base.transaction do
  david.withdrawal(100)
  mary.deposit(100)
end
Transationのロールバックが発火条件
Railsでは、ロールバックが発火するには、「例外」が必要である。これがTransactionを使うときのもっとも重要なことである。
例えば、Railsでは #update_attributeは例外を発火せずに、falseに返すとなる。そのため、#update_attributeを使うには、結果を見て、例外をスローする必要がある。Railsではびっくりマーク!がついているメソッドは、失敗したら例外をスローすると意図するので、transactionを使うときは、saveではなくsave!、destroyではなくdestroy!を使うべきでしょう。
例外が発生するときは、transactionがまず受け取って、ロールバックを行う。ロールバックが終わった後は、例外がそのまま外にスローされるため、アプリケーション側もその例外を対応する必要がある。
例外を使わずに、Transactionをロールバックさせたい場合は、ActiveRecord::Rollbackを使えばよい。ActiveRecord::Rollbackは外側にスローされないため、アプリケーション側は対応しなくてもいいという利点がある。
いつNested Transactionを使うべき
Nested Transactionの例ですが:
User.transaction do
  User.create(username: 'Kotori')
  User.transaction do
    User.create(username: 'Nemu')
    raise ActiveRecord::Rollback
  end
end
Nested Transactionを使うときの最も重要な注意点は、一番中の処理がActiveRecord::Rollbackが投げても、子transactionで受け取った後広められず、親Transactionは受け取れないことである。広めたいときは、子transactionに :require_new => trueを設定する必要がある。
じゃいつNested Transactionを使うべきかの話ですが、マルチデータベースをマルチモデルで使うときは、メソッドをNested Transctionの中で囲まれる必要がある:
Client.transaction do
  Product.transaction do
    product.buy(@quantity)
    client.update_attributes!(:sales_count => @sales_count + 1)
  end
end
Transactionsでのコールバック
もし#saveとdestroyがTransactionで使う場合、#after_saveもTransactionの中で実行されてしまう。そのため
Transactionの外で実行したい場合は、#after_commitと#after_rollbackで実装してください。
Transactionで細かいの注意点
Transactionの中で ActiveRecord::Invalidをしない方がよい。なぜなら、Postgresではその例外をキャッチしたら、囲まれるTransactionもロールバックされてしまう振る舞いがある。
結論
Transactionを使うときのアンチパータン
- シングルレコードを更新するときにTransaction使う
- Nested Transactionを使いすぎる
- Transactionにある処理がどうしてもロールバックを発火できない
- ControllerでTransactionを使う
参考資料