3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qiita株式会社Advent Calendar 2024

Day 2

Rails (ActiveRecord) の Transaction で ネストやら after_commit を試してみた

Last updated at Posted at 2024-12-25

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
app/models/book.rb
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
app/models/publisher.rb
class Publisher < ApplicationRecord
+  validates :name, presence: true
end
app/models/book.rb
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 をトリガーしますが、トランザクション ブロックによって再度発生することはありません。その他の例外は再度発生します。

  • 例が微妙だったかも……
3
0
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
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?