package DBIx::TransactionManager; use strict; use warnings; use Carp (); our $VERSION = '1.08'; sub new { my ($class, $dbh) = @_; unless ($dbh) { Carp::croak('missing mandatory parameter dbh'); } bless { dbh => $dbh, active_transactions => [], rollbacked_in_nested_transaction => 0, }, $class; } sub txn_scope { DBIx::TransactionManager::ScopeGuard->new( @_ ); } sub txn_begin { my ($self, %args) = @_; my $caller = $args{caller} || [ caller(0) ]; my $txns = $self->{active_transactions}; push @$txns, { caller => $caller, pid => $$ }; if (@$txns == 1) { $self->{dbh}->begin_work; } } sub txn_rollback { my $self = shift; my $txns = $self->{active_transactions}; return unless @$txns; my $current = pop @$txns; if ( @$txns > 0 ) { # already popped, so if there's anything, we're in a nested # transaction, mate. $self->{rollbacked_in_nested_transaction}++; } else { $self->{dbh}->rollback; $self->_txn_end; } } sub txn_commit { my $self = shift; my $txns = $self->{active_transactions}; return unless @$txns; if ( $self->{rollbacked_in_nested_transaction} ) { Carp::croak "tried to commit but already rollbacked in nested transaction."; } my $current = pop @$txns; if (@$txns == 0) { $self->{dbh}->commit; $self->_txn_end; } } sub _txn_end { @{$_[0]->{active_transactions}} = (); $_[0]->{rollbacked_in_nested_transaction} = 0; } sub active_transactions { $_[0]->{active_transactions} } sub in_transaction { my $self = shift; my $txns = $self->{active_transactions}; return @$txns ? $txns->[0] : (); } package DBIx::TransactionManager::ScopeGuard; use Try::Tiny; sub new { my($class, $obj, %args) = @_; my $caller = $args{caller} || [ caller(1) ]; $obj->txn_begin( caller => $caller ); bless [ 0, $obj, $caller, ], $class; } sub rollback { return if $_[0]->[0]; $_[0]->[1]->txn_rollback; $_[0]->[0] = 1; } sub commit { return if $_[0]->[0]; $_[0]->[1]->txn_commit; $_[0]->[0] = 1; } sub DESTROY { my($dismiss, $obj, $caller) = @{ $_[0] }; return if $dismiss; warn( "Transaction was aborted without calling an explicit commit or rollback. (Guard created at $caller->[1] line $caller->[2])" ); try { $obj->txn_rollback; } catch { die "Rollback failed: $_"; }; } 1; __END__ =head1 NAME DBIx::TransactionManager - transaction handling for database. =head1 SYNOPSIS basic usage: use DBI; use DBIx::TransactionManager; my $dbh = DBI->connect('dbi:SQLite:'); my $tm = DBIx::TransactionManager->new($dbh); $tm->txn_begin; $dbh->do("insert into foo (id, var) values (1,'baz')"); $tm->txn_commit; scope_gurad usage: use DBI; use DBIx::TransactionManager; my $dbh = DBI->connect('dbi:SQLite:'); my $tm = DBIx::TransactionManager->new($dbh); my $txn = $tm->txn_scope; $dbh->do("insert into foo (id, var) values (1,'baz')"); $txn->commit; nested transaction usage: use DBI; use DBIx::TransactionManager; my $dbh = DBI->connect('dbi:SQLite:'); my $tm = DBIx::TransactionManager->new($dbh); { my $txn = $tm->txn_scope; $dbh->do("insert into foo (id, var) values (1,'baz')"); { my $txn2 = $tm->txn_scope; $dbh->do("insert into foo (id, var) values (2,'bab')"); $txn2->commit; } { my $txn3 = $tm->txn_scope; $dbh->do("insert into foo (id, var) values (3,'bee')"); $txn3->commit; } $txn->commit; } =head1 DESCRIPTION DBIx::TransactionManager is a simple transaction manager. like L<DBIx::Class::Storage::TxnScopeGuard>. =head1 METHODS =head2 my $tm = DBIx::TransactionManager->new($dbh) get DBIx::TransactionManager's instance object. $dbh parameter must be required. =head2 my $txn = $tm->txn_scope(%args) get DBIx::TransactionManager::ScopeGuard's instance object. You may pass an optional argument to %args, to tell the scope guard where the scope was generated, like so: sub mymethod { my $self = shift; my $txn = $tm->txn_scope( caller => [ caller() ] ); } $self->mymethod(); This will allow the guard object to report the caller's location from the perspective of C<mymethod()>, not where C<txn_scope()> was called. see L</DBIx::TransactionManager::ScopeGuard's METHODS> =head2 $tm->txn_begin(%args) Start the transaction. C<txn_begin> may optionally take a 'caller' argument. This will allow you to provide caller information which will be used in C<in_transaction>. For example if you have a wrapper function that calls C<txn_begin>, you may want to let the user think that the caller was one stack above your wrapper. # use __my__ caller! $tm->txn_begin( caller => [ caller(0) ] ); =head2 $tm->txn_rollback Rollback the transaction. =head2 $tm->txn_commit Commit the transaction. =head2 $tm->in_transaction Returns true if $txn is currently in a middle of a transaction. While normally you only need to use this value as a boolean, it actually returns a hashref consisting of 'caller' and 'pid' element. This will tell you exactly where the currently valid transaction started. =head1 DBIx::TransactionManager::ScopeGuard's METHODS =head2 $txn->commit Commit the transaction. =head2 $txn->rollback Rollback the transaction. =head1 AUTHOR Atsushi Kobayashi E<lt>nekokak _at_ gmail _dot_ comE<gt> =head1 SEE ALSO L<DBIx::Class::Storage::TxnScopeGuard> L<DBIx::Skinny::Transaction> =head1 LICENSE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut