IT・技術研修ならCTC教育サービス

サイト内検索 企業情報 サイトマップ

研修コース検索

コラム

enjoy Railsway

CTC 教育サービス

 [IT研修]注目キーワード   Python  UiPath(RPA)  最新技術動向  Microsoft Azure  Docker  Kubernetes 

第2回 ActiveRecord::Base.transaction ロールバック編 (泉隼人) 2015年4月

 こんにちは、トランスネットの泉です。
Ruby on Railsについてのコラムenjoy Railsway、第2回は「ActiveRecord::Base.transaction ロールバック編」 をお送りします。
複数のモデルを一度に更新するような処理をおこなう場合、原子性を担保するためにトランザクションを考慮した実装となるはずです。
fig01

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がロールバックされる条件

 transactionがロールバックされる条件は、「ブロック内で例外が発生する」ことです。

saveメソッドは、保存に失敗したときにfalseを返しますが、例外を発生させません。
そのため、transactionをロールバックさせるためには、保存に失敗したときに例外を発生させるsave!メソッドを利用します。

saveとsave!

 Railsのドキュメントから、二つのメソッドの挙動の違いを引用します。

save
  • バリデーションやコールバックで保存がキャンセルされると false を返す
save!
  • バリデーションで保存がキャンセルされると ActiveRecord::RecordInvalid が発生する
  • コールバックで保存がキャンセルされると ActiveRecord::RecordNotSaved が発生する

例を一目見ただけでお気付きの方も多いと思いますが、barの保存にはsaveメソッドを使っていたため、例外が発生せず、ロールバックも発生しなかったのです。

このように!ひとつで大きな差が出てしまうため、私がコードレビューを実施する際は入念にチェックしています。
また、save!しても差し支えないような状況では、saveではなくsave!を普段から積極的に使うように働きかけています。
(そもそも、モデルの保存に失敗したことをケアしなくていいような処理は稀なはずです...)

ActiveRecord::Rollback

 ちなみに、自分で例外を発生させてロールバックさせるような場合は、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は正しくロールバックされたようです。

Reading Rails!

 ところで、この挙動について書籍などでもあまり詳しく説明されないことがあるようです。
幸い、ActiveRecordのソースコードは誰でも読むことができます。
折角ですので、実際のところActiveRecord内部ではどのような仕組みになっているのか確認してみることにします。

※ここではGitHubのリポジトリタグv4.1.6時点のソースコードを参照しています。

rails / activerecord / lib / active_record / transactions.rb

206

207

208

209

def transaction(options = {}, &block)

 # See the ConnectionAdapters::DatabaseStatements#transaction API docs.

 connection.transaction(options, &block)

end

ConnectionAdapters::DatabaseStatements#transactionを見てみます。

rails / activerecord / lib / active_record / connection_adapters / abstract / database_statements.rb

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で捕捉されることはないことを確認することができました。

rails / activerecord / lib / active_record / connection_adapters / abstract / database_statements.rb

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