概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Rails has deprecated using return, break and throw to exit a transaction | Saeloun Blog
- 原文公開日: 2020/04/06
- 著者: Rohit Kumar
- サイト: Saeloun Blog | Ruby on Rails Consulting Company based in San Francisco and Boston | Page 1 | Saeloun Blog
Rails 6.1でreturnやbreakやthrowによるトランザクション終了が非推奨化(翻訳)
Railsには長年の間、トランザクションをひそかにコミットする機能が入っていました(fc83920)。トランザクションの内部でreturn
を呼び出すと、コネクション上のトランザクションを開きっぱなしにしない形でreturn
が実行されます。
この機能は、何らかの条件が満たされない場合にトランザクションから早々に抜け出すのにも使えます。
def destroy_post_if_invalid
Post.transaction do
post = Post.find_by(id: id)
return if post.valid?
post.destroy
end
end
トランザクションが裏でコミットされるこの振る舞いは、Rubyのtimeout
メソッドと組み合わせたときに驚かされることがあります。
以下の例では、トランザクションが1秒以内に終了しなかった場合にもコミットされてしまいます。
Timeout.timeout(1) do
Post.transaction do
# 何か重たい処理を行う
# post.time_consuming_task
# ここで何らかの遅い動作をシミュレートする
sleep 3
end
end
このように動作するのは、klass
引数が渡されない場合にtimeout
がthrow
でトランザクションブロックを終了するためです。
# throwとcatchでブロックを終了する
>> Timeout.timeout(1) do
sleep 2
end
Timeout::Error: execution expired
# ArgumentErrorをraiseする
>> Timeout.timeout(1, ArgumentError) do
sleep 2
end
ArgumentError: execution expired
このプルリクの作者は、この驚きの振る舞いを非推奨化するにあたって代替手段を設けなかったので、この問題に関する後方互換のソリューションはありません。
残念ながら、ensureブロックではブロックの終了がreturnで行われたのか、breakで行われたのか、throwで行われたのかを区別できません。このため、
Timeout.timeout
で行われているようにthrow
でこの問題を修正することができません。
ここでRails 6.1での以下の非推奨化warningを見てみましょう。
DEPRECATION WARNING: Using `return`, `break` or `throw` to exit a transaction block is
deprecated without replacement. If the `throw` came from
`Timeout.timeout(duration)`, pass an exception class as a second
argument so it doesn't use `throw` to abort its block. This results
in the transaction being committed, but in the next release of Rails
it will raise and rollback.
先に進めるソリューションは、以下のようにトランザクション内部でreturn
ではなくif
やunless
による条件を利用するか、timeout
メソッドを持つ何らかの例外クラスを用いることです。
def destroy_post_if_invalid
Post.transaction do
post = Post.find_by(id: id)
unless post.valid? do
post.destroy
end
end
end