Railsを使っている中で、「トランザクションの中だとモデルのid列はいつ設定されるのか」が気になったことから、確認してみたときのメモを残します。
目次
環境
- Rails 7.1.3
- DBはデフォルトのSQLite3
また、今回は以下のモデルとマイグレーションを用意します。
apple.rb
class Apple < ApplicationRecord end
モデルに対するマイグレーションファイルはこちら。
class CreateApples < ActiveRecord::Migration[7.1] def change create_table :apples do |t| t.string :name t.timestamps end end end
動作確認
上記のAppleモデルに対して、
- step1. トランザクションの中で、
build
した直後 - step2. トランザクションの中で、
save
した直後 - step3. トランザクションの外で、
build
した直後 - step4. トランザクションの外で、
save
した直後
の各タイミングで、モデルオブジェクトの id
がどのように設定されるかを確認します。
まずは以下の rake task を作成します。
namespace :print_model_id_with_transaction do desc 'トランザクション利用時のmodelのidを確認' task run: :environment do # rake taskだと、`config.active_record.verbose_query_logs = true` であってもSQLログが出ないので、設定しておく # ActiveRecord::Base.logger = Logger.new(STDOUT) # Rails.logger.level = Logger::DEBUG ActiveRecord::Base.transaction do apple = Apple.build(name: 'シナノゴールド') puts "step1: plan.id => #{apple.id}" apple.save puts "step2: plan.id => #{apple.id}" raise ActiveRecord::Rollback end apple_without_transaction = Apple.build(name: '奥州ロマン') puts "step3: plan.id => #{apple_without_transaction.id}" apple_without_transaction.save puts "step4: plan.id => #{apple_without_transaction.id}" end end
続いて rake task を実行したところ
$ bin/rails print_model_id_with_transaction:run step1: plan.id => step2: plan.id => 1 step3: plan.id => step4: plan.id => 1
となりました。
これより、
- トランザクションの中の挙動
build
では、id
には何も設定されないsave
を実行した後、id
にDBで採番した値が設定される
- トランザクションをロールバックしたら
id
列の採番も元に戻り、次にINSERTした時にも同じ番号が設定される- これは SQLite を使っているのでこのような挙動になる
- PostgreSQLやMySQLであれば、おそらく挙動は違う模様 (番号が再利用されない)
- トランザクションの外の挙動
という挙動と分かりました。トランザクションの中と外で設定されるタイミングは同じなようです。
次に、実際に発行されるSQLも見てみます。
今回は rake task で動作確認をしているため、task の先頭に以下を追加します。
rakeタスクでクエリのログを標準出力に出す | このコードわからん
task run: :environment do # 追加 ActiveRecord::Base.logger = Logger.new(STDOUT) Rails.logger.level = Logger::DEBUG # 以下同じ ActiveRecord::Base.transaction do ...
再度 rake task を実行すると、以下のような感じで表示されました(横長になったので、タイムスタンプ部分を削除しています)。
トランザクションの中では、 commit transaction
がなくてもモデルの id 列に値が設定されるようです。
$ bin/rails print_model_id_with_transaction:run D, : TRANSACTION (0.1ms) begin transaction D, : ↳ lib/tasks/print_model_id_with_transaction.rake:10:in `block (3 levels) in <top (required)>' step1: plan.id => D, : Apple Create (0.2ms) INSERT INTO "apples" ("name", "created_at", "updated_at") VALUES (?, ?, ?) RETURNING "id" [["name", "シナノゴールド"], ["creat "2024-02-17 11:22:40.829966"], ["updated_at", "2024-02-17 11:22:40.829966"]] D, : ↳ lib/tasks/print_model_id_with_transaction.rake:12:in `block (3 levels) in <top (required)>' step2: plan.id => 2 D, : TRANSACTION (0.1ms) rollback transaction D, : ↳ lib/tasks/print_model_id_with_transaction.rake:9:in `block (2 levels) in <top (required)>' step3: plan.id => D, : TRANSACTION (0.1ms) begin transaction D, : ↳ lib/tasks/print_model_id_with_transaction.rake:20:in `block (2 levels) in <top (required)>' D, : Apple Create (1.0ms) INSERT INTO "apples" ("name", "created_at", "updated_at") VALUES (?, ?, ?) RETURNING "id" [["name", "奥州ロマン"], ["created_a2024-02-17 11:49:59.591712"], ["updated_at", "2024-02-17 11:49:59.591712"]] D, : ↳ lib/tasks/print_model_id_with_transaction.rake:20:in `block (2 levels) in <top (required)>' D, : TRANSACTION (7.3ms) commit transaction D, : ↳ lib/tasks/print_model_id_with_transaction.rake:20:in `block (2 levels) in <top (required)>' step4: plan.id => 2
余談:Railsのバージョンにより、トランザクション内での return 等の挙動が変わる
本題とは関係ない内容です。
Railsのトランザクションまわりを調べていたところ、Railsのバージョンによりトランザクションの挙動が変わると知りました。
- Rails 6.1でreturnやbreakやthrowによるトランザクション終了が非推奨化(翻訳)|TechRacho by BPS株式会社
- トランザクションがreturn、break、throwでコミットするようになった | 週刊Railsウォッチ: Rails 7.0.5のcreate_association挙動変更取り消し、YJITの性能を最大限引き出す方法ほか(20230809)|TechRacho by BPS株式会社
- Rails 7.1.0 Active Record CHANGELOG(翻訳)|TechRacho by BPS株式会社
- Rails 7.1 から、 return 等でのコミットする挙動がデフォルトとなる模様
- Rails 7.1.0 Active Record CHANGELOG(翻訳)|TechRacho by BPS株式会社
ソースコード
Githubに上げました。
https://github.com/thinkAmi-sandbox/rails_7_1_minimal_app
今回のプルリクはこちら。
https://github.com/thinkAmi-sandbox/rails_7_1_minimal_app/pull/1