DBIx::TransactionManagerのメカニズム
#!/usr/bin/env perl use strict; use warnings; use DBI; use DBIx::TransactionManager; use Try::Tiny; use Test::More; my $dbh = DBI->connect("dbi:SQLite:"); my $tm = DBIx::TransactionManager->new($dbh); $dbh->do(q{ CREATE TABLE user (id INTEGER PRIMARY KEY, name TEXT) }); #------------------------------------------------ package MyModel; sub insert_commit { my ($class, $name) = @_; my $txn = $tm->txn_scope; $dbh->do(q{ INSERT INTO user (name) VALUES ( ? ) }, undef, $name); $txn->commit; } sub insert_not_commit { my ($class, $name) = @_; my $txn = $tm->txn_scope; $dbh->do(q{ INSERT INTO user (name) VALUES ( ? ) }, undef, $name); # 敢えてコミットしない } #-------------------------------------------------------------------- package main; try { { my $txn1 = $tm->txn_scope; MyModel->insert_commit('a'); $dbh->do(q{ INSERT INTO user (name) VALUES ('b') }); $txn1->commit; } { # 外側のscope1 my $txn2 = $tm->txn_scope; $dbh->do(q{ INSERT INTO user (name) VALUES ('c') }); MyModel->insert_commit('d'); # 内側のscope1 MyModel->insert_not_commit('e'); # 内側のscope2 $txn2->commit; } } catch { warn $_; }; my $ret = $dbh->selectall_arrayref(q{ SELECT * FROM user }, { Columns => +{} }); is_deeply $ret, [ { id => 1, name => 'a'}, { id => 2, name => 'b'}, ]; done_testing;
ネストの状態とか自動rollbackとかどうやってるんだろうと気になったのでソースを読んでみた。
まずtxn_scope(正確にはtxn_scope内のtxn_begin)が実行されるたびにインクリメント、txn_commit,txn_rollbackが実行されるたびにデクリメントされるactive_transactionという内部パラメータがある。
このactive_transactionというパラメータでネストの状態を管理して値が1の時に実際に$dbhのcommitないしrollbackが実行されているよう。
またcommitをしないでscopeを抜けるとScopeGuardオブジェクトのDESTROYでtxn_rollbackが実行されるという仕組みになっているみたい。なのでrollbackを自分で呼び出さなくもいい。
rollbackされるケースの2つ目のトランザクションについてactive_transactionを中心に詳細を見ると
1 (外側のscope1)txn_scopeでactive_transaction++
active_transaction => 1
2 (内側のscope1)insert_commit内txn_scopeでactive_transaction++
active_transaction => 2
3 (内側のscope1)insert_commit内txn_commitでactive_transaction--
active_transaction => 1
4 (内側のscope2)insert_not_commit内txn_scopeでactive_transaction++
active_transaction => 2
5 (内側のscope2)commitされずにscopeを抜けるのでDESTORYでtxn_rollbackが実行されactive_transaction--。またrollbacked_in_nested_transaction++。ここではまだ実際にはrollbackされない。
active_transaction => 1
rollbacked_in_nested_transaction => 1
6 (外側のscope1)rollbacked_in_nested_transactionが1の状態でcommitが実行されるとcroak("try to commit 〜")が実行されscopeを抜ける。active_transactionのデクリメントは行われない。
active_transaction => 1
rollbacked_in_nested_transaction => 1
7 (外側のscope1)scopeを抜ける際のDESTORYでtxn_rollback。ここで$dbhのrollbackが実行される。またtxn_rollback内でtxn_endが実行されactive_transactionは0になる。
active_transaction => 0
rollbacked_in_nested_transaction => 0
以上。
パラメータのインクリメントとデクリメントでネストを管理、
scopeを抜けるタイミングでDESTORYというのが他でも色々使えそうそうで大変ためになったのでした。