課題
Rails 6.1 を採用してテストを実施していたら以下のような警告が
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 rollback.
(called from transactional_create_detail at /Users/pharaohkj/Dropbox/osx/gitwork/learn_transaction_6.1/app/models/user.rb:21)
return
や break
や throw
でトランザクションブロックを抜けるの、いまはトランザクションをコミットするけど、次のリリースからはロールバックするからな。
とのこと。えっ?これまでコミットしてたんだっけ???
つまるところ
これまでロールバックしたかったら model.class.transaction do
から end
のブロックにおいて raise
しろって話だったのに、それ以外の制御構文、例えばreturnでブロック抜けた場合もロールバックするよって話。
修正不要、課題にならないところ
transaction 中に !
系のメソッドを用いる、カスタム例外を発生させるなどしていて正しくロールバックしている場合 ( そもそも DEPRECATED 警告もでない )はこれまでどおり。
修正必要な課題になってしまうところ
transaction 中に return などの制御文で抜けた場合、その制御文までがコミット される ことを期待したコード(例えば、下のサンプルでは、従属レコードが保存されなくてもUser自体は保存されて欲しい場合)は、挙動が変わってロールバックされてしまう。
こんなの
update(point: 0)
self.class.transaction do
update(point: 1)
update(point: 2)
return unless update(point: 3) # updateに失敗したらreturn
update(point: 4)
update(point: 5)
end
↑の結果、これまでは self
.point
はロールバックされずに 2 。 今後ロールバックされて 0
。
なんかでかいバッチのループの回数とか保存してるときにこういうことやるかもね。
update(point: 0)
begin
self.class.transaction do
update!(point: 1)
update!(point: 2)
update!(point: 3) # ここでupdateに失敗して例外
update!(point: 4)
update!(point: 5)
end
rescue
end
↑の結果、これまでは self
.point
はロールバックされて 0 。今後も 0
。
以下、確認
サンプル準備
アプリを作って、ユーザーモデルを作成し、そのユーザー詳細テーブルを作ってリレーションさせる。
ユーザー詳細テーブルは有効なユーザーレコードのIDに従属する(ユーザーレコードがなければ保存できない)
bundle init
bundle install --path vendor/bundle
bundle exec rails new ./
bundle exec rails g scaffold user
bundle exec rails g scaffold user_detail user:references
bundle exec rails db:migrate
bundle exec rails webpacker:install
class User < ApplicationRecord
has_one :user_detail
end
とりあえずここまでで準備完了。 http://localhost:3000/users
にアクセスしてユーザー作成できることを確認。
課題のコード
さて User
に User#create_detail` を作成し、まず自分自身を保存し、成功したらその自分自身用の詳細を保存する。
もしも詳細の作成に失敗してしまったら、そもそも自分自身の保存もなかったこと = ロールバックしたいというコードを作って検証する。
...
def create_with_detail
transactional_create_detail
puts "after function persisted? => #{persisted?}, id => #{id}"
rescue => e
puts "*after function on rescue* persisted? => #{persisted?}, id => #{id}, e => #{e.class}"
end
...
def transactional_create_detail
puts __method__
self.class.transaction do
save_result = save # ここで保存して
puts "save result => #{save_result}" #
puts "persisted? => #{persisted?}, id = #{id}" #
ud = UserDetail.create # ここで詳細を作成して
return if ud.id.nil? # return で抜けたら、、、userはsaveされている?いない?
puts '***** FINISH *****'
end
end
...
トランザクション開始し、いったん自分を保存、そのあと UserDetail
も作成する。 、、、んだけど、この課題のコードではリレーションするUserの指定をわざと忘れてcreateしているので、UserDetailレコードの作成に失敗し、id は nil になって if 文に引っかかって return
でトランザクション閉じる end
までいかないので user はロールバックされて保存はなかったことになる!
のか、はてさてそれとも
トランザクション開始し、いったん自分を保存、そのあと UserDetail
も作成する。 、、、んだけど、この課題のコードではリレーションするUserの指定をわざと忘れてcreateしているので、UserDetailレコードの作成に失敗し、id は nil になって if 文に引っかかって return
でトランザクション閉じるが、特に例外が発生でこのブロックから例外で抜けるわけではないので user は保存される!
のか。
まぁなんでレコードを扱う系のメソッドに !
がついているものがあるのか知っている人はご存じの通り。
答えは後者。Userはちゃんと 保存される。 が正解である。
・・・正解だろ?それがDEPRECATEDになるというのが今回の課題である。
さて、この実行結果は以下の通り、userはロールバックされず、保存されている。ただし、今後はそれはロールバックするからな!とのこと。
transactional_create_detail
TRANSACTION (0.1ms) begin transaction
↳ app/models/user.rb:22:in `block in transactional_create_detail'
User Create (0.4ms) INSERT INTO "users" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2021-03-17 12:49:50.557593"], ["updated_at", "2021-03-17 12:49:50.557593"]]
↳ app/models/user.rb:22:in `block in transactional_create_detail'
save result => true
persisted? => true, id = 31
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 rollback.
(called from transactional_create_detail at /Users/pharaohkj/Dropbox/osx/gitwork/learn_transaction_6.1/app/models/user.rb:21)
以下いろいろ検証したコード
結局transaction中にraise以外でロールバックしたつもり、、、がコミットされてた、、、ってのが次のリリースでマジでロールバックになっちまうって話。
サンプルコード
class User < ApplicationRecord
has_one :user_detail
def create_detail
transactional_create_detail
# transactional_create_detail_with_rescue
# transactional_create_detail_with_rescue2
# transactional_create_detail_with_rescue3
# transactional_create_detail_with_uncatch
puts "after function persisted? => #{persisted?}, id => #{id}"
rescue => e
puts "*after function on rescue* persisted? => #{persisted?}, id => #{id}, e => #{e.class}"
end
def transactional_create_detail
puts __method__
self.class.transaction do
save_result = save
puts "save result => #{save_result}" # ここで保存して
puts "persisted? => #{persisted?}, id = #{id}" #
ud = UserDetail.create # ここで詳細を作成して
return if ud.id.nil? # return で抜けたら、、、userはsaveされている?いない?
puts '***** FINISH *****'
end
end
def transactional_create_detail_with_rescue
puts __method__
self.class.transaction do
save_result = save
puts "save result = #{save_result}" # ここで保存して
puts "persisted = #{persisted?}, id = #{id}" #
ud = UserDetail.create! # ここで詳細を作成して
puts '***** FINISH *****' #
rescue => e # ここで例外を受け取ったら、、、userはsaveされている?いない?
puts '***** RESCUE! *****'
puts e
end
end
def transactional_create_detail_with_rescue2
puts __method__
self.class.transaction do
save_result = save
puts "save result = #{save_result}" # ここで保存して
puts "persisted = #{persisted?}, id = #{id}" #
ud = UserDetail.create! # ここで詳細を作成して
puts '***** FINISH *****'
end
rescue => e # transaction外で例外を受け取ったら、、、userはsaveされている?いない?
puts '***** OVER RESCUE! *****'
puts e
end
def transactional_create_detail_with_rescue3
puts __method__
self.class.transaction do
save_result = save
puts "save result = #{save_result}" # ここで保存して
puts "persisted = #{persisted?}, id = #{id}" #
ud = UserDetail.create! # ここで詳細を作成して
puts '***** FINISH *****'
rescue => e # ここで例外を受け取って投げて
puts '***** RESCUE! *****'
raise e
end
rescue => e # ここで例外を受け取ったら、、、userはsaveされている?いない?
puts '***** OVER RESCUE! *****'
puts e
end
def transactional_create_detail_with_uncatch
puts __method__
self.class.transaction do
save_result = save
puts "save result = #{save_result}" # ここで保存して
puts "persisted = #{persisted?}, id = #{id}" #
ud = UserDetail.create! # ここで詳細を作成して失敗して例外が飛んだらuserはsaveされている?いない?
end
end
end