Railsにおけるネストしたtransactionの挙動
内側のトランザクションブロックは外側の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