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というのが他でも色々使えそうそうで大変ためになったのでした。