« ハローバイバイ・関暁夫の都市伝説―信じるか信じないかはあなた次第 | Home | 2006 夜景 六本木ヒルズ »

[Catalyst]   Catalyst::Model::DBIC::Schemaでのトランザクション管理

以前

Catalyst::Model::DBIC::Plainでのトランザクション管理

というエントリーを書きました。
Catalyst::Model::DBIC::Plainってあたりが時代を感じさせます。

Cookbokにもありますが現在のDBIx::Classでトランザクションを管理する場合には$schema->txn_do($coderef)を使うとトランザクションの開始からコミット、ロールバックまでやってくれます。

my $schema = CD::Schema->connect(...);
$txn = sub {
    my $artist = $schema->resultset('Artists')->create(
        {
            name => $name,
        }
    );
    my $album = $schema->resultset('Albums')->create(
        {
            title => $title,
            artist => $artist->id,
        }
    );
}

eval { $schema->txn_do($txn); };

if ($@) {
    if ($@ =~ /Rollback failed/) {
        die "rollback failed: $@"
    }
    else {
        die "rollbacked: $@"
    }
}

Catalyst::Model::DBIC::Schemaで使うときには

sub do_add : Local {
    my ( $self, $c ) = @_;

    ...

    my $txn = sub {
        my $artist = $c->model('CD::Artists')->create(
            {
                name => $c->req->param('name'),
            }
        );
        my $album = $c->model('CD::Albums')->create(
            {
                title  => $c->req->param('title'),
                artist => $artist->id
            }
        );
    };
    $c->do_txn($c->model('CD'), $txn );

    eval { $c->model('CD')->schema->txn_do($txn); };
    if ($@) {
        if ( $@ =~ /rollback failed/ ) {
            $c->error("rollback failed: $@");
        }
        else {
            $c->error("rollbacked: $@");
        }
    }

    ...
}

これを毎回やるのは面倒です。

で、MLを見てたらMST曰く

[Catalyst] Best practice for using transactions?
You could always subclass Catalyst::Controller's _DISPATCH private  
action like

sub _DISPATCH {
   my $self = shift;
   my ($c) = @_;
   $c->model('DB')->schema->txn_do(
     sub {
       $self->next::method(@_);
     }
   );
}

つまり_DISPATCHメソッドをオーバーライドしたMyApp::Base::Controllerとか継承して全てのディスパッチに対してトランザクションを組んじゃえ・・・というとっても豪快な解がありました。

ボクの場合は局所的に使いたいのでプラグインを作ってこんな感じにしてます。

package MyApp::Plugin::Utils;

use strict;
use warnings;

sub do_txn {
    my ( $c, $model, $txn ) = @_;

    eval { $model->schema->txn_do($txn); };
    if ($@) {
        if ( $@ =~ /rollback failed/ ) {
            $c->error("rollback failed: $@");
        }
        else {
            $c->error("rollbacked: $@");
        }
    }
}

で、MyApp.pmでロード

package MyApp;

use strict;
use warnings;

use Catalyst::Runtime '5.70';

use Catalyst qw/
    -Debug
    Dumper
    StackTrace
    ConfigLoader
    Static::Simple
    +MyApp::Plugin::Utils
/;

...

で、コントローラでこんな感じ

sub do_add : Local {
    my ( $self, $c ) = @_;

    ...

    my $txn = sub {
        my $artist = $c->model('CD::Artists')->create(
            {
                name => $c->req->param('name'),
            }
        );
        my $album = $c->model('CD::Albums')->create(
            {
                title  => $c->req->param('title'),
                artist => $artist->id
            }
        );
    };
    $c->do_txn($c->model('CD'), $txn );

    ...
}

・・・あまり短くなってない orz
・・・けどエラー処理は一箇所にまとめられます。

もちろんInnoDBなどのトランザクション対応なデータベースエンジンを使わなければ意味はありません。

Trackbacks:

このエントリーのトラックバックURL:

Comments (3)

有益な情報の提供ありがとうございます。
特にDBIx::Classについてはまだまだ情報が少ないので、非常に助かっております。

私もエラー処理を何とか共通化出来ないものかと、いろいろ探していたのですが、DEVELOPER RELEASEである0.07999に、exception_action というメソッドが追加されています。

関数リファレンスを指定しておくと、throw_exception が発生した場合に呼んでくれます。

今、使ってみているのですが、エラー発生時に別のテンプレートの表示や、エラー内容の取得も出来るので便利だと思います。
ただ引数にはエラー内容しか渡されないので、オブジェクトを使うには関数内に入れておくしかなく、循環参照になってしまうので、weakenしておく必要があります。

と、これならひでさんの方法のほうが良いですね。
この exception_action、何かうまい使い方がありそうなら、ぜひ教えてください。

tamashiro様>
お返事遅くなってすみません。

exception_actionに関しては不勉強で知りませんでした。
ちらっとソースを見た感じでexception_actionは全ての例外発生時に呼ばれるよ
うなのでトランザクションを組まない(組めない)場合のエラーハンドルで独自に処
理したいことがある時に使うのがいいのかなぁと思います。
ただ、ボクの場合、Catalyst以外でDBICを使うことがほとんどないのでエラー処
理は全てCatalystに任せるようにしています。

hideさん
お返事いただきありがとうございます。

> トランザクションを組まない(組めない)場合のエラーハンドルで独自に処理したいことがある時に使うのがいいのかなぁと思います。

そうですね、SELECT1つ呼ぶ場合でもエラーを捕捉出来るという事ですね。

> エラー処理は全てCatalystに任せるようにしています。

Catalystではそういった事が可能なのですね。
まだまだ勉強が足りません。。

ありがとうございました。

コメントを投稿