Railsのトランザクションとその注意点

Railsのトランザクションは、ActiveRecordの標準機能として手軽に利用できます。

トランザクションとは、 ACID的な意味で2つ以上の処理をAtomicに実行する機能で、銀行の口座間振替処理の例がよく引き合いに出されます。

たとえばaとbの2つのモデルオブジェクトをsaveする場合、極端に簡略化すると以下のようにActiveRecord::Base.transactionのブロックとして実装した部分がアトミックなトランザクション処理になります。

ActiveRecord::Base.transaction do
  a.save
  b.save
end

ただし、このコードでは問題が起きます。

ロールバック処理に注意が必要

トランザクションのポイントは、いずれかのオブジェクトの保存が失敗した際にすべての処理をロールバックする点にあります。

ところがActiveRecordのトランザクションは、ブロック内の処理が例外を起こさない限り実はロールバック処理をしません。

DBMSにPostgreSQLを利用している場合、あるトランザクションが途中で止まって、commitもrollbackもされずに宙に浮いた場合、後続のトランザクション要求がエラーになります。

実ケースの例でいうと、あるユーザーの処理途中でトランザクションが完結しなかったら、後続の別ユーザーが同じ処理を通るとデータを保存できない状態になります。

そのため、トランザクション機能を利用する場合には、個別のDB操作メソッドが完了しない場合のエラー処理に注意を払う必要があります。

DB操作メソッドに要注意

トランザクションを安全に使うためには、使用する場面を意識的に限定することと、ActiveRecordのDB操作メソッドの挙動に詳しくなることが必要です。

先ほどのケースでいうと、savesave!が違うということを知っておくということです。

save!は失敗すると例外を返すため、連動してトランザクションもrollbackされます。
一方、saveメソッドはセーブできなかった場合、falseが返るのでこちらの実装では結果を判定して手動でraise処理を実装する必要があります。

saveの場合はまだ単純ですが、BULK INSERTのためにdelete_allやactiverecord-importを使うケースなどでは、ドキュメントが完備していないのでライブラリの実装を追って行く必要が出てきます。

まとめ

Railsのトランザクションを使いこなすためには、ActiveRecord::Base.transactionよりも、個々のDB操作メソッドの挙動について理解する方が重要です。

トランザクション内のエラーはテストケースでカバーすることも難しく、後続の処理でエラーが起こることも多いため難易度は高い印象があります。

中馬崇尋
Chuma Takahiro