👻

Railsにおけるネストしたtransactionの挙動

2021/06/13に公開

内側のトランザクションブロックは外側のtransactionの一部となる

class User < ApplicationRecord
  after_save { raise ActiveRecord::Rollback }
end

# consoleの中
ActiveRecord::Base.transaction do
  user = User.first # => User { name: "hoge" }
 user.update!(name: "new hoge")
end

上記のコードではUserモデルのインスタンスを保存しています。callbacksはrailsがtransactionを作りその中で実行されるので、consoleで作ったtransactionにネストする形で新しいtransactionが始まり、after_saveの処理が実行されます。そしてこの場合、rollbackされずにuserのnameは"new hoge"に更新されてしまいます。
理由は内側のトランザクションブロックはROLLBACKを発行しないからです。transactionをネストした場合、内側のtransaction内のSQL文は外側のtransactionの一部となります。そしてその場合内側ではROLLBACKは発行されません。従って、内側でActiveRecord::RollbackがraiseされてもROLLBACKは発行されません。また、ActiveRecord::Rollbackは他の例外と異なり、ActiveRecord::Base.transactionに補足された後に再raiseされません。よって外側のtransactionからはROLLBACKが見えず例外も補足出来ない為、transactionはcommitされてしまいます。

内側のtransactionを外側のtransactionの一部にしない方法

前述の通りrailsではネストしたtransactionの中身は外側のtransactionの一部となります。外側のtransactionが内側のtransactionを回収しないようにするには、joinable: falseを渡します。

ActiveRecord::Base.transaction(joinable: false) do
  user = User.first # => User { name: "hoge" }
 user.update!(name: "new hoge")
end

外側のtransactionとは別のtransactionを開始する方法

ネストした時に新しいtransactionを開始するにはrequire_new: trueを渡してActiveRecord::Base.transactionを呼び出します。

ActiveRecord::Base.transaction do
   User.create!(name: "hoge")
   ActiveRecord::Base.transaction(require_new: true) do
      User.create!(name: "huga")
      raise ActiveRecord::Rollback
   end
end

上記のコードではUser { name: 'hoge' }は作られ、User { name: 'huga' }は作られません。

Discussion