この記事は trocco® Advent Calendar 2023
21日目の記事になります。
こんにちは。trocco開発メンバーのいとうです。最近一気に寒くなりましたね。
この記事では最近ハマっていたRailsのトランザクション処理について書こうと思います。(何番煎じだよ)
先に結論だけ書くと、よくわからず使っているという私みたいな人は公式をちゃんと読み解いて、コード読んだり
実際に手を動かして書いてみると理解できると思います。
なので、私の記事は読まなくていいので、APIリファレンス読んでない人は読んでおくと良いです。
最初にまとめ
-
ActiveRecord::Base.transaction
ブロックはネストされても1つのトランザクションとして動作する。 - ネストした内部で
ActiveRecord::Rollback
が起きても伝搬されない(=ロールバックされない - ネストした内部で他の例外が発生した場合は大元のtransactionブロックまで到達すればロールバックされる
- ネストした内部のtransactionブロックでトランザクション処理が動かないわけでは無い。
ネストした内部で ActiveRecord::Rollback
が起きても伝搬されない
Railsフレームワークにおいて、transactionブロックをネストした形での記述は可能で、
その場合、全てのtransactionブロックが、親のトランザクションの一部となります。
参考のコードは公式そのままです。
User.transaction do
User.create(username: 'Kotori')
User.transaction do
User.create(username: 'Nemu')
raise ActiveRecord::Rollback
end
end
# 例外通知こない
# User.all.count ==> 2
公式でも書いてあるとおり、Kotori、Nemuのユーザは作成されます。
私は最初「え、なんで?」と思ったのですがその後に記載されているとおり、
ActiveRecord::Rollback
は一番近いtransactionブロックでキャプチャされてしまい、親には見えていないためとのことです。
ネストした内部で他の例外が発生した場合は大元のtransactionブロックまで到達すればロールバックされる
あくまで ActiveRecord::Rollback
が親まで伝搬されないということなので
別の例外であれば親まで伝搬され、ロールバックされます。
User.transaction do
User.create(username: 'Kotori')
User.transaction do
User.create(username: 'Nemu')
# raise ActiveRecord::Rollback
raise StandardError("ロールバックしたい")
end
end
# 例外通知来る
# User.all.count ==> 0
これなら直感的にわかりますね。
ネストされたブロックでの ActiveRecord::Rollback
の扱いが特殊なんですね。
ネストした内部のtransactionブロックが動かないわけでは無い
私が業務でハマったのはこれで、見ている箇所ではtransactionブロックにしているのに
ロールバック処理されていないことがわかり調査を始めました。
RSpecのメソッドの単体テストではロールバックできているのにな...と。
この原因はtransactionブロックでネストされた ActiveRecord::Rollback
の動作と同じで、ネストしたtransactionブロックの中で例外を握り潰していたことで、親のブロックまで到達せずロールバックされていないことがわかりました。
User.transaction do
User.create(username: 'Kotori')
User.transaction do
User.create(username: 'Nemu')
# ...
rescue StandardError => e
logger.error("うまく処理できなかった")
end
end
# User.all.count ==> 2
こんだけスッキリ読めれば「それはそうだよね」でしかないのですが
実務のコードはまぁそんなことはなくて、
今読んでいるところが大元のtransactionブロックかと思いきや、
さらに上位の呼び出し元でtransactionブロックを使っていたことがわかり、特定に時間がかかったということです。
あえて擬似コードに落とし込むとしたらこんな感じですかね。
(これでもまぁシンプルではありますが)
# user_creation.rb
class UserCreation
def run()
User.transaction do
User.create(username: 'Nemu')
Item.transaction do
Item.create('pencil case')
Item.create('pencil') # --> error
end
end
end
end
# root_executor.rb
ActiveRecord::Base.transaction do # おっとここから transaction ブロックはじまってるじゃん。
UserCreation.new.run()
rescue StandardError => e
logger.error("作成に失敗")
end
end
user_creation.rbまでしかみてなかったので、なんで単体テストではロールバックもできるのに実際にウェブサービスとして動かしてみるとデータが残ってしまうのだ?という状態でした。
さいごに
最後まで読んでいただきありがとうございました。
troccoはユーザの利用する画面をRuby On Rails、
今回は書けませんでしたが、バックエンドで embulk(Java実装のOSS)のプラグインはもちろん
最近だとGoやC++のコードがあることに気づいてとてもわくわくしています。
Rustも業務で使ってみたいですね。
troccoの開発に興味ある人はぜひ連絡ください。