[Catalyst-commits] r12386 - trunk/examples/CatalystAdvent/root/2009

zamolxes at dev.catalyst.perl.org zamolxes at dev.catalyst.perl.org
Tue Dec 15 07:12:31 GMT 2009


Author: zamolxes
Date: 2009-12-15 07:12:31 +0000 (Tue, 15 Dec 2009)
New Revision: 12386

Added:
   trunk/examples/CatalystAdvent/root/2009/15.pod
Log:
recovered day 15


Added: trunk/examples/CatalystAdvent/root/2009/15.pod
===================================================================
--- trunk/examples/CatalystAdvent/root/2009/15.pod	                        (rev 0)
+++ trunk/examples/CatalystAdvent/root/2009/15.pod	2009-12-15 07:12:31 UTC (rev 12386)
@@ -0,0 +1,179 @@
+=head1 Transactions and DBIx::Class
+
+Database transactions help ensure the integrity and consistency of your data.
+Here I will present a few (contrived) examples where they are useful.
+
+=head2 Atomicity
+
+Often a Model operation, in the sense of business logic, requires a group of
+closely related (or dependent) database operations.  These should be
+atomic.  That is if any single operation in the group fails, then the whole
+group of operations will fail and your data remains consistent.
+
+Suppose you wrote some code such as:
+
+    sub create_user : Local {
+        my ($self, $c) = @_;
+
+        # we love hash slices
+        my ($username, $first_name, $last_name, $token_id) =
+            @{ $c->req->params }{qw/username first_name last_name token_id/};
+
+        unless ($c->model('Token')->validate($token_id)) {
+            $c->flash->{error} = "Bad token: $token_id";
+            $c->detach('/error');
+        }
+
+        my $user = $c->model('DB::User')->create({
+            username => $username,
+            first_name => $first_name,
+            last_name => $last_name,
+        });
+
+        $user->add_to_tokens({ token_id => $token_id });
+
+        $c->flash->{message} = "Created user ${username}!";
+    }
+
+This looks perfectly reasonable, but there are two operations here, one which
+creates a user entry, and one that creates a token entry for it.
+
+What could possibly go wrong between them? Lots of things, such as the web
+server going down in flames, the database server crashing, alien invasion, and
+so on. While the chances of this are small, repairing erroneous data in a
+database is a painful, expensive and hazardous process.
+
+Also we had to use an unnatural control flow here, normally we'd validate the
+token before it is created, rather than before the user is created.
+
+Enter C<txn_do>, from L<DBIx::Class::Schema>.
+
+    $c->model('DB')->txn_do(sub {
+        my $user = $c->model('DB::User')->create({
+            username => $username,
+            first_name => $first_name,
+            last_name => $last_name,
+        });
+
+        $c->model('Token')->validate($token_id) or die "Bad token: $token_id";
+
+        $user->add_to_tokens({ token_id => $token_id });
+    });
+
+Now that the operation is atomic, your user entries as well as any related
+entries are guaranteed to be consistent, and we are using a more natural
+control flow.
+
+Any exceptions from C<txn_do> are re-thrown and a C<ROLLBACK> is issued,
+leaving the database untouched by the code enclosed by the transaction.
+
+Exceptions can be cleaned up in your C<end> handler, by getting them from C<<
+$c->error >>, or using something like
+L<Catalyst::Action::RenderView::ErrorHandler>. However, in some cases you may
+want to wrap them in an eval (or one of L<Try::Catch>, L<Try::Tiny>) to detach
+in order to short-circuit a chain for example.
+
+It is also recommended to factor out database code into the relevant
+L<DBIx::Class::ResultSet> methods. ResultSet classes go into the
+C<lib/MyApp/Schema/ResultSet> directory (for example), next to the C<Result>
+directory and inherit from C<DBIx::Class::ResultSet>.
+
+=head2 txn_scope_guard
+
+A different control flow for transactions is possible with C<txn_scope_guard>,
+from L<DBIx::Class::Storage>.
+
+With this method, an object is created and a transaction is started; if the
+object goes out of scope before C<< ->commit >> is called on it, via an
+exception or any other means, a C<ROLLBACK> will be issued.
+
+This can be useful if you don't want to deal with catching exceptions from
+C<txn_do>.  Another reason you might choose to use C<txn_scope_guard> over
+C<txn_do> is that C<txn_do> will attempt to reconnect to the database and
+re-run your code if you lost your connection; C<txn_scope_guard> does not
+have this overhead.
+
+Here's the previous example using C<txn_scope_guard>:
+
+    sub create_user : Local {
+        my ($self, $c) = @_;
+
+        my ($username, $first_name, $last_name, $token_id) =
+            @{ $c->req->params }{qw/username first_name last_name token_id/};
+
+        my $scope_guard = $c->model('DB')->txn_scope_guard;
+
+        my $user = $c->model('DB::User')->create({
+            username => $username,
+            first_name => $first_name,
+            last_name => $last_name,
+        });
+
+        unless ($c->model('Token')->validate($token_id)) {
+            $c->flash->{error} = "Bad token: $token_id";
+            $c->detach('/error');
+        }
+
+        $user->add_to_tokens({ token_id => $token_id });
+
+        $c->flash->{message} = "Created user ${username}!";
+
+        $scope_guard->commit;
+    }
+
+Here on C<< $c->detach >> the C<$scope_guard> goes out of scope, and a
+C<ROLLBACK> is issued, as well as on any exception. The changes are committed
+at the end of the action.
+
+=head2 Nested Transactions
+
+Many databases support a feature called 'savepoints', which are nested
+transactions. They allow you to roll back a group of operations that are part
+of a larger group of operations.
+
+L<DBIx::Class> supports savepoints for the MySQL, Oracle, Postgres, MSSQL
+and Sybase databases.
+
+You have to add C<< auto_savepoint => 1 >> to your C<connect_info> to enable
+this feature:  see L<DBIx::Class::Storage::DBI> for details.
+
+    sub validate_batch : Local {
+        my ($self, $c) = @_;
+        my @urls = split "\n", $c->req->param('urls');
+
+        $c->model('DB')->txn_do(sub {
+            my $batch = $c->user->add_to_batches({});
+            my $validated_count = 0;
+
+            for my $url (@urls) {
+                try {
+                    $c->model('DB')->txn_do(sub {
+                        my $url = $c->model('DB::Url')->find_or_create({ url => $url });
+                        my $result = $c->model('Validator')->validate($url);
+                        $batch->add_to_urls({ url => $url->id, result => $result });
+                        $validated_count++;
+                    });
+                } catch {
+                    push @{ $c->flash->{errors} }, $_;
+                }
+            }
+
+            die "Could not validate any URLs" unless $validated_count;
+
+            $batch->update({ url_count => $validated_count });
+        });
+    }
+
+In this example, we are validating a batch of URLs, if any single URL fails it
+will not be added to any tables in the database, if they all fail then nothing
+will be added to the database.
+
+=head2 Transactions, a good idea!
+
+As you can see, the L<DBIx::Class> facilities for database transactions can
+make your code safer and simpler.
+
+=head1 AUTHOR
+
+Caelum: Rafael Kitover <rkitover at cpan.org>
+




More information about the Catalyst-commits mailing list