7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

何番煎じかわからないけどRailsのトランザクション処理について

Last updated at Posted at 2023-12-22

この記事は 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の開発に興味ある人はぜひ連絡ください。

参考

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?