When you work with a relational database, sometimes writing safe and correct programs requires you to bundle up several database changes into a single transaction which you can apply or reject atomically.
In DBIx::Class,
that's through the txn_do
method, which looks something like:
$db->txn_do(sub
{
# make changes here
...
# make other changes here
});
Pass an anonymous function to this method. The method will start a transaction, invoke the anonymous function, then commit or roll back the transaction depending on the success or failure of the anonymous function. It's a good pattern, because the anonymous function can do arbitrary things; the transactional wrapper imposes very few requirements on it.
(PSGI follows a similar pattern.)
In a recent commit on a client project, I wanted to add transactional semantics to a DDL change. In effect, if creating a custom database view failed, I wanted to roll back the change without replacing an existing view. If replacing an existing view succeeded, I wanted to commit it.
I could have used the anonymous function pattern, but I chose a different approach:
sub run_in_transaction {
my ($self, $method_name, $args) = @_;
$self->begin_work;
local $@;
my $status = eval { $self->$method_name( @$args ); $self->commit; 1 };
return $status if $status;
$self->rollback;
die $@;
}
Instead of wrapping up the other code in a transactional block, to use this method you pass the name of another method in the same class to call along with an array reference of arguments to pass to that method.
Is this easier to use? In some circumstances, but it also has its limitations. It only works for methods on the same object. It only works for single statements. When those conditions are true, I think this code is clearer> than the anonymous function approach.
Will it last in our code after a few more rounds of refactorings and revisions? I don't know—but it solved the DDL update problem elegantly enough for now.
DBIC can't get away with anything this simple, because it needs to provide a consistent and coherent solution without imposing artificial limitations. This code has no need for that generality yet. I could just as well have used the anonymous function, and I may in the future, but for now this satisfies my needs. It's just another technique which you may find useful in specific circumstances.
> This code has no need for that generality yet. I could just as well have used the anonymous function, and I may in the future, but for now this satisfies my needs.
With this I cannot disagree.
> When those conditions are true, I think this code is clearer
Disagree.
# perl 5.10+
MyClass->transaction {
$myobject->mymethod($args)
}
looks clearer.
1) transaction code is most likely should be class method, not instance method. if your class (not object) does not
contain required data (i.e. database connection handle) to perform the transaction, it's just implementation problem.
2) method name has no meaning without object, and in your example it's passed as separate argument.
btw, in Ruby such things always implemented like
transaction do
...
end
Were you using Postgres as your database? If you were, then you wouldn't have to "add transactional semantics to a DDL change" as DDL is already subject to transactions (I think there's only one exception), so you can rollback DDL. Even SQLite does that too. I seem to recall no other SQL DBMS does, unfortunately. This feature is a huge point in the favor of Postgres. (In contrast, I recall MySQL will do an implicit commit of prior work if you perform any DDL, which is truly a bad thing.)
I'm surprised you didn't use Try::Tiny in there. Is there a reason for that?
This code is part of a reusable component and I didn't want to force the use of Try::Tiny for other projects. I know what Try::Tiny fixes and it doesn't do anything in this case that the code you see doesn't already do. The syntactic advantage wasn't quite enough in that case.
We are using Pg, but I'm paranoid enough at this step that I want an explicit savepoint, especially if the view update method ends up performing more than one DDL modification.
Try::Tiny is *exceptionally* heavy. I am in the process of taking it out of parts of DBIC, because in some hot-spots it accounts for over 50% of the entire execution time.
Ideally I need to find some time to peruse Filter::Keyword[1] (not yet on CPAN) to inline-rewrite try/catch invocations into the corresponding eval. Then Try::Tiny will be truly useful again.
[1] http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=p5sagit/Filter-Keyword.git
It is slightly off-topic, but is there really a way in 5.10+ how to write a method call without parenthesis and sub keyword? I doubt it even though
MyClass->transaction {
$myobject->mymethod($args)
}
looks more elegant than ordinary
MyClass->transaction(sub {
$myobject->mymethod($args)
})
Not to my knowledge, no.
There is, but need to use '::', not '->'
use 5.010;
package MyClass;
my $handle = 123;
sub transaction(&) { say "begin"; shift->(); say "commit ($handle) "; }
1;
package main;
my $handle = 345;
MyClass::transaction {
say "aha"
};
1;
__END__
begin
aha
commit (123)
(tested on 5.10.1)
The database connection itself is an instance variable, so it's either methods or pass the connection as a parameter for my code.