ActiveRecord で,rollback するかどうか
うろ覚えだし,いまいちよくわからなかったので,試してみた.
とりあえずテストするモデルを作る
準備
$ devcontainer up --workspace-folder .
作る
$ devcontainer exec --workspace-folder . rails generate model Book title:string author:string
$ devcontainer exec --workspace-folder . rails db:migrate
class Book < ApplicationRecord
+ validates :title, presence: true, uniqueness: true
+ validates :author, presence: true
end
試す
- ROLLBACK ということになっている
- 今回は,INSERT より前(validate) 時に発行しているので,RDBMS で ROLLBACK したっぽくはない.
$ devcontainer exec --workspace-folder . rails c
sample-rails(dev)> Book.create!
TRANSACTION (4.5ms) BEGIN /*application='SampleRails'*/
Book Exists? (22.5ms) SELECT 1 AS one FROM `books` WHERE `books`.`title` IS NULL LIMIT 1 /*application='SampleRails'*/
TRANSACTION (0.8ms) ROLLBACK /*application='SampleRails'*/
(sample-rails):7:in `<main>': Validation failed: Title can't be blank, Author can't be blank (ActiveRecord::RecordInvalid)
sample-rails(dev)>
- シンプルなトランザクションは期待通りに動作する
- 一度は RDBMS に INSERT したデータが無くなっていることがわかる
ActiveRecord::Base.transaction do
Book.create!(title: 'a', author: 'b')
Book.create!
end
sample-rails(dev)* ActiveRecord::Base.transaction do
sample-rails(dev)* Book.create!(title: 'a', author: 'b')
sample-rails(dev)* Book.create!
sample-rails(dev)> end
TRANSACTION (0.5ms) BEGIN /*application='SampleRails'*/
Book Exists? (2.5ms) SELECT 1 AS one FROM `books` WHERE `books`.`title` = 'a' LIMIT 1 /*application='SampleRails'*/
Book Create (0.9ms) INSERT INTO `books` (`title`, `author`, `created_at`, `updated_at`) VALUES ('a', 'b', '2024-12-16 05:49:22.342478', '2024-12-16 05:49:22.342478') /*application='SampleRails'*/
Book Exists? (0.9ms) SELECT 1 AS one FROM `books` WHERE `books`.`title` IS NULL LIMIT 1 /*application='SampleRails'*/
TRANSACTION (3.5ms) ROLLBACK /*application='SampleRails'*/
(sample-rails):14:in `block in <main>': Validation failed: Title can't be blank, Author can't be blank (ActiveRecord::RecordInvalid)
from (sample-rails):12:in `<main>'
sample-rails(dev)> Book.count
Book Count (0.5ms) SELECT COUNT(*) FROM `books` /*application='SampleRails'*/
=> 0
トランザクションのネスト
- ロールバックできる
- ログから見ると,トランザクションがネストされている訳ではないかも
-
transaction calls can be nested. By default, this makes all database statements in the nested transaction block become part of the parent transaction.
ActiveRecord::Base.transaction do
Book.create!(title: 'a', author: 'b')
ActiveRecord::Base.transaction do
Book.create!
end
end
sample-rails(dev)* ActiveRecord::Base.transaction do
sample-rails(dev)* Book.create!(title: 'a', author: 'b')
sample-rails(dev)* ActiveRecord::Base.transaction do
sample-rails(dev)* Book.create!
sample-rails(dev)* end
sample-rails(dev)> end
TRANSACTION (0.5ms) BEGIN /*application='SampleRails'*/
Book Exists? (11.5ms) SELECT 1 AS one FROM `books` WHERE `books`.`title` = 'a' LIMIT 1 /*application='SampleRails'*/
Book Create (0.6ms) INSERT INTO `books` (`title`, `author`, `created_at`, `updated_at`) VALUES ('a', 'b', '2024-12-23 09:50:34.848218', '2024-12-23 09:50:34.848218') /*application='SampleRails'*/
Book Exists? (0.5ms) SELECT 1 AS one FROM `books` WHERE `books`.`title` IS NULL LIMIT 1 /*application='SampleRails'*/
TRANSACTION (1.9ms) ROLLBACK /*application='SampleRails'*/
(sample-rails):4:in `block (2 levels) in <main>': Validation failed: Title can't be blank, Author can't be blank (ActiveRecord::RecordInvalid)
from (sample-rails):3:in `block in <main>'
from (sample-rails):1:in `<main>'
sample-rails(dev)> Book.count
Book Count (1.7ms) SELECT COUNT(*) FROM `books` /*application='SampleRails'*/
=> 0
commmit 後の失敗
- トランザクションをネストしている訳ではないが,commit 後に 失敗するとどうなるか試した
$ devcontainer exec --workspace-folder . rails generate model Publisher name:string
$ devcontainer exec --workspace-folder . rails db:migrate
class Publisher < ApplicationRecord
+ validates :name, presence: true
end
class Book < ApplicationRecord
+ after_create_commit do
+ Publisher.create!(name: "Publisher #{title}" && nil) # わざと失敗する
+ end
+
validates :title, presence: true, uniqueness: true
validates :author, presence: true
end
- test
ActiveRecord::Base.transaction do
Book.create(title: 'a', author: 'b')
end
$ devcontainer exec --workspace-folder . rails c
sample-rails(dev)* ActiveRecord::Base.transaction do
sample-rails(dev)* Book.create(title: 'a', author: 'b')
sample-rails(dev)> end
TRANSACTION (0.4ms) BEGIN /*application='SampleRails'*/
Book Exists? (2.1ms) SELECT 1 AS one FROM `books` WHERE `books`.`title` = 'a' LIMIT 1 /*application='SampleRails'*/
Book Create (0.5ms) INSERT INTO `books` (`title`, `created_at`, `updated_at`, `author`) VALUES ('a', '2024-12-25 07:09:36.667896', '2024-12-25 07:09:36.667896', 'b') /*application='SampleRails'*/
TRANSACTION (2.9ms) COMMIT /*application='SampleRails'*/
app/models/book.rb:3:in `block in <class:Book>': Validation failed: Name can't be blank (ActiveRecord::RecordInvalid)
from (sample-rails):3:in `<main>'
sample-rails(dev)> Book.count
Book Count (1.8ms) SELECT COUNT(*) FROM `books` /*application='SampleRails'*/
=> 1
sample-rails(dev)> Publisher.count
Publisher Count (0.5ms) SELECT COUNT(*) FROM `publishers` /*application='SampleRails'*/
=> 0
- 特にrollback はしなかった
終了
$ docker compose -f .devcontainer/compose.yaml stop
Refs
Exception handling and rolling back
Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you should be ready to catch those in your application code.One exception is the ActiveRecord::Rollback exception, which will trigger a ROLLBACK when raised, but not be re-raised by the transaction block. Any other exception will be re-raised.
Exception処理とロールバック
また、トランザクション ブロック内でスローされた例外は (ROLLBACK をトリガーした後) 伝播されることに留意し、アプリケーション コードでそれらをキャッチできるように準備しておく必要があります。1 つの例外はActiveRecord::Rollback、発生したときに ROLLBACK をトリガーしますが、トランザクション ブロックによって再度発生することはありません。その他の例外は再度発生します。
- 例が微妙だったかも……