Active Recordに永続化メソッド用のトランザクションカスタマイズポイントを追加
Active Recordでは、saveやdestroyなどの永続化メソッドが暗黙的に作成するトランザクションをカスタマイズできる implicit_persistence_transaction メソッドが追加されました。これにより、モデルごとにトランザクション分離レベルの指定や、既存トランザクション内での二重トランザクション回避が可能になります。
背景
Active Recordの永続化メソッド(save、destroy、touch)は内部で自動的にトランザクションを作成しますが、このトランザクションの動作をカスタマイズする標準的な手段がありませんでした。特定の分離レベル(:read_committedなど)を使用したい場合、開発者は次のいずれかの方法を取る必要がありました。
既存の対処法には以下の課題がありました:
-
明示的なトランザクションブロックで毎回ラップする: 呼び出し箇所すべてで
Account.transaction(isolation: :read_committed) { account.save }のように記述する必要があり、コードの重複が発生 -
with_transaction_returning_statusメソッド全体をモンキーパッチする: 上流での変更に対して脆弱で、メンテナンス性が低い
ActiveRecord.with_transaction_isolation_level はブロック内のグローバルな分離レベル設定を提供しますが、モデル単位で永続化メソッドのトランザクション動作を設定する手段ではありません。#56673 では save_transaction_options メソッドによるオプション指定が検討されましたが、より柔軟な制御が求められていました。
技術的な変更
activerecord/lib/active_record/transactions.rb の with_transaction_returning_status メソッドが、トランザクション作成部分を新しい implicit_persistence_transaction メソッドに委譲するよう変更されました。
変更前:
def with_transaction_returning_status
status = nil
ensure_finalize = !connection.transaction_open?
connection.transaction do
add_to_transaction(ensure_finalize || has_transactional_callbacks?)
remember_transaction_record_state
status = yield
rescue ActiveRecord::Rollback
status = nil
end
status
end
変更後:
def with_transaction_returning_status
status = nil
ensure_finalize = !connection.transaction_open?
implicit_persistence_transaction(connection) do
add_to_transaction(ensure_finalize || has_transactional_callbacks?)
remember_transaction_record_state
status = yield
rescue ActiveRecord::Rollback
status = nil
end
status
end
新しく追加された implicit_persistence_transaction メソッドは以下のシグネチャを持ちます:
def implicit_persistence_transaction(connection, &block)
connection.transaction(&block)
end
このメソッドをモデル内でオーバーライドすることで、トランザクション作成をカスタマイズできます。デフォルト実装は単純に connection.transaction を呼び出すため、既存の動作は変わりません。
使用例として、既にトランザクションが開いている場合にネストトランザクションを回避する実装は以下のようになります:
class Account < ApplicationRecord
private
def implicit_persistence_transaction(connection, &block)
if connection.transaction_open?
yield
else
super
end
end
end
テストコードでは、このメソッドが永続化操作時に確実に呼び出されることと、既存トランザクション内での動作が検証されています。activerecord/test/cases/transactions_test.rb に追加された test_implicit_persistence_transaction_is_called_when_in_nested_transaction テストでは、connection.current_transaction.open? を使用してトランザクションの有無を判定し、適切に制御できることを確認しています。
設計判断
メソッドオーバーライドによる拡張ポイント というアプローチが採用されました。
代替案として検討された #56673 の save_transaction_options メソッドは、トランザクションオプションのHashを返す形式でした。しかし、PR本文で指摘されているように、このアプローチでは以下の制約がありました:
- トランザクション作成自体をスキップする判断ができない
-
ActiveRecord::Rollback例外やthrowのカスタムハンドリングができない - コールバック内で発生する可能性のある例外処理を追加できない
これに対し、implicit_persistence_transaction はトランザクション作成全体を制御できるため、以下のユースケースに対応可能です:
-
connection.transaction_open?による条件分岐でのトランザクション作成制御 - 分離レベルの指定:
connection.transaction(isolation: :read_committed, &block) - トランザクション前後での追加処理の実行
- カスタムエラーハンドリングの実装
メソッド名に「implicit」が含まれているのは、明示的な Model.transaction 呼び出しとの区別を明確にするためです。また、private メソッドとして定義されることで、このフックがフレームワーク内部向けの拡張ポイントであることが示されています。
まとめ
本PRは、永続化メソッドのトランザクション動作をモデル単位でカスタマイズする標準的な方法を提供します。単一のメソッドオーバーライドポイントを導入することで、トランザクション作成のロジック全体に対する柔軟な制御を可能にしながら、既存コードへの影響を最小限に抑えています。これにより、モンキーパッチに頼らずに、アプリケーション固有のトランザクション要件に対応できるようになりました。