DBIx::TransactionManager の目的と、その使用法について
おはようございます。
DBI では当たり前のように $dbh->do('BEGIN') と $dbh->do('COMMIT') をつかえばトランザクションがつかえるわけですが、なぜ DBIx::TransactionManager のようなものが必要になったのでしょうか。
それは勿論、DBI で直接 transaction をとりあつかうと問題が発生するケースが存在するからです。
トランザクションと RAII
一番おおきいのは、トランザクションが中途半端な状態になってしまうことを阻止することです。たとえば、以下のようなケースでは、おかしなことになってしまいます。
my $dbh = DBI->connect(...); for (@stuff) { eval { $dbh->do("BEGIN"); $dbh->do(q{INSERT INTO t1 (v) VALUES (?)}, $_); $dbh->do(q{INSERT INTO t2 (v) VALUES (?)}, $_*2); $dbh->do("COMMIT"); }; warn $@ if $@; }
途中の INSERT でエラーになったりした場合でも、そのまま処理が続けられてしまいます。まあこの場合だと以下のように書きかえればすむ話なんですが、コードが複雑になってくると、確実に rollback するのがむずかしくなってきます。また、ぱっとみたときに、確実にrollback されているかを検証するのが面倒になってきます。
if ($@) { warn $@; $dbh->do('ROLLBACK'); }
単純なトランザクションを1個うつだけのスタンドアロンスクリプトなら、手で ROLLBACK をうっていてもいいのですが、web application などで永続的なコネクションをはっている場合には、中途半端なトランザクションの状態のコネクションとか再利用してしまうと絶望的ですよね。
さて、ここで、 DBIx::TransactionManager をつかうと、RAII によって、明示的にコミットされない場合はスコープをぬけるタイミングで rollback がかかるので、トランザクションの状態がもれることがなくなります。
my $dbh = DBI->connect(...); my $tm = DBIx::TransactionManager->new($dbh); for (@stuff) { eval { my $txn = $tm->txn_scope; $dbh->do(q{INSERT INTO t1 (v) VALUES (?)}, $_); $dbh->do(q{INSERT INTO t2 (v) VALUES (?)}, $_*2); $tm->commit; }; warn $@ if $@; }
トランザクションのネスト
もうひとつの大きなメリットはトランザクションのネストに対応していることです。以下のようなコードが自然にかけます。また、一箇所でエラーがおきると一番上までロールバックしてくれるので中途半端な状態でコミットされません。
my $dbh = ...; my $tm = DBIx::TransactionManager->new($dbh); &foo; exit; sub foo { my $txn = $tm->txn_scope(); $dbh->do(q{INSERT INTO ...}); bar(); $txn->commit; } sub bar { my $txn = $tm->txn_scope(); $dbh->do(q{INSERT INTO ...}); $txn->commit; }
DBIx::TransactionManager においては一応 $tm->txn_begin とか $tm->txn_commit などのメソッドも明示的によべますが、これよんでると $dbh を直接つかってるのとかわりがないので、$tm->txn_scope 一本槍でかくのがいいです。基本的には my $txn = $tm->txn_scope と $txn->commit そして $txn->rollback ぐらいのメソッドだけをつかうのがこのライブラリの基本的な使い方です(それ以外のメソッドは、これをベースにしてライブラリを作成したい人のために用意されているものです)
まとめ
以上、簡単ですが DBIx::TransactionManager の紹介でした。