LoginSignup
2
1

More than 3 years have passed since last update.

vs Rails 6.1. "Using `return`, `break` or `throw` to exit a transaction block is deprecated without replacement."

Last updated at Posted at 2021-03-17

課題

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)

returnbreakthrow でトランザクションブロックを抜けるの、いまはトランザクションをコミットするけど、次のリリースからはロールバックするからな。

とのこと。えっ?これまでコミットしてたんだっけ???

つまるところ

これまでロールバックしたかったら 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に従属する(ユーザーレコードがなければ保存できない)

  1. bundle init
  2. bundle install --path vendor/bundle
  3. bundle exec rails new ./
  4. bundle exec rails g scaffold user
  5. bundle exec rails g scaffold user_detail user:references
  6. bundle exec rails db:migrate
  7. bundle exec rails webpacker:install
app/models/user.rb
class User < ApplicationRecord
  has_one :user_detail
end

とりあえずここまでで準備完了。 http://localhost:3000/users にアクセスしてユーザー作成できることを確認。

課題のコード

さて User に User#create_detail` を作成し、まず自分自身を保存し、成功したらその自分自身用の詳細を保存する。
もしも詳細の作成に失敗してしまったら、そもそも自分自身の保存もなかったこと = ロールバックしたいというコードを作って検証する。

app/models/user.rb
...
  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以外でロールバックしたつもり、、、がコミットされてた、、、ってのが次のリリースでマジでロールバックになっちまうって話。

サンプルコード

app/models/user.rb
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
2
1
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
2
1