CTC 教育サービス
[IT研修]注目キーワード Python UiPath(RPA) 最新技術動向 Microsoft Azure Docker Kubernetes
こんにちは、トランスネットの泉です。
Ruby on Railsについてのコラムenjoy Railsway、第2回は「ActiveRecord::Base.transaction ロールバック編」 をお送りします。
複数のモデルを一度に更新するような処理をおこなう場合、原子性を担保するためにトランザクションを考慮した実装となるはずです。
Ruby on Railsでの開発では、 ActiveRecord::Base.transaction を利用することになります。
さてこのtransactionですが、使い方を間違えてしまうとうまく機能しません。
ActiveRecord::Baseを継承したクラス、FooとBarがあるとします。
次の例のfoo、barはこれらのクラスのインスタンスです。
barのバリデーション結果がfalseとなっているため保存に失敗します。
そのため、同じトランザクション内でsaveしたfooの状態も巻き戻るはずです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
puts foo.updated_at # => 2014-10-06 12:00:00 +0900 puts bar.updated_at # => 2014-10-06 12:00:00 +0900
puts foo.valid? # => true puts bar.valid? # => false
ActiveRecord::Base.transaction do foo.save bar.save end
puts foo.updated_at # => 2014-10-07 09:00:00 +0900 puts bar.updated_at # => 2014-10-06 12:00:00 +0900 |
view rawenjoy_railsway_2-1.rb hosted with ♥ byGitHub
foo.updated_atの値が変更されたようです。
...fooが更新されてしまいました。
transactionがロールバックされる条件は、「ブロック内で例外が発生する」ことです。
saveメソッドは、保存に失敗したときにfalseを返しますが、例外を発生させません。
そのため、transactionをロールバックさせるためには、保存に失敗したときに例外を発生させるsave!メソッドを利用します。
Railsのドキュメントから、二つのメソッドの挙動の違いを引用します。
例を一目見ただけでお気付きの方も多いと思いますが、barの保存にはsaveメソッドを使っていたため、例外が発生せず、ロールバックも発生しなかったのです。
このように!ひとつで大きな差が出てしまうため、私がコードレビューを実施する際は入念にチェックしています。
また、save!しても差し支えないような状況では、saveではなくsave!を普段から積極的に使うように働きかけています。
(そもそも、モデルの保存に失敗したことをケアしなくていいような処理は稀なはずです...)
ちなみに、自分で例外を発生させてロールバックさせるような場合は、ActiveRecord::Rollbackを使うことができます。
raise ActiveRecord::Rollback
この例外はtransactionの外側では捕捉されません。
この点については後ほどご紹介します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
puts foo.updated_at # => 2014-10-06 12:00:00 +0900 puts bar.updated_at # => 2014-10-06 12:00:00 +0900 puts bar.valid? #=> false
begin ActiveRecord::Base.transaction do foo.save! raise ActiveRecord::Rollback unless bar.save # 保存に失敗すると例外発生 end rescue ActiveRecord::Rollback # この箇所が実行されることは無い end
puts foo.updated_at # => 2014-10-06 12:00:00 +0900 puts bar.updated_at # => 2014-10-06 12:00:00 +0900 |
view rawenjoy_railsway_2-2.rb hosted with ♥ byGitHub
foo.updated_atの値が変更されていません。
...fooは正しくロールバックされたようです。
ところで、この挙動について書籍などでもあまり詳しく説明されないことがあるようです。
幸い、ActiveRecordのソースコードは誰でも読むことができます。
折角ですので、実際のところActiveRecord内部ではどのような仕組みになっているのか確認してみることにします。
※ここではGitHubのリポジトリ、タグv4.1.6時点のソースコードを参照しています。
206 207 208 209 |
def transaction(options = {}, &block) # See the ConnectionAdapters::DatabaseStatements#transaction API docs. connection.transaction(options, &block) end |
ConnectionAdapters::DatabaseStatements#transactionを見てみます。
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
def transaction(options = {}) options.assert_valid_keys :requires_new, :joinable, :isolation
if !options[:requires_new] && current_transaction.joinable? if options[:isolation] raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction" end
yield else within_new_transaction(options) { yield } end rescue ActiveRecord::Rollback # rollbacks are silently swallowed end |
within_new_transactionメソッド内で処理がおこなわれそうです。
また、 rescue ActiveRecord::Rollback という記述が確認できます。
ActiveRecord::Rollbackはここで拾われるため、transactionの外側のrescueで捕捉されることはないことを確認することができました。
207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
def within_new_transaction(options = {}) #:nodoc: transaction = begin_transaction(options) yield rescue Exception => error rollback_transaction if transaction raise ensure begin commit_transaction unless error rescue Exception rollback_transaction raise end end |
within_new_transactionメソッド内でトランザクションの開始処理の後、ブロック内の処理がおこなわれていることを確認できます。
注目点は、rollback_transactionメソッドの呼び出しです。このメソッドは、このwithin_new_transactionメソッド内で例外をrescueした場合にのみ呼び出されています。
つまり、「transaction内では例外が発生したときのみ、ロールバックが発生する」ことになります。
これでActiveRecord::Base.transactionの挙動を確認することができました。
ActiveRecord::Base.transactionでのロールバック処理についてご紹介しました。
「saveではうまくいかなかったが、save!したらうまくいった!」といった表面的なところに留まらずに、「何故そうなるか」をご理解いただけましたでしょうか。
こうして実装を確認して頭に入れておくことで、「transactionブロックを書いたのに、ロールバックされない...!」というような悩みから解放されると思います:-)
それでは今日はこの辺で。次回をお楽しみに♪
[IT研修]注目キーワード Python UiPath(RPA) 最新技術動向 Microsoft Azure Docker Kubernetes