[Catalyst-commits] r12415 - trunk/examples/CatalystAdvent/root/2009/pen

ribasushi at dev.catalyst.perl.org ribasushi at dev.catalyst.perl.org
Thu Dec 17 00:09:22 GMT 2009


Author: ribasushi
Date: 2009-12-17 00:09:21 +0000 (Thu, 17 Dec 2009)
New Revision: 12415

Added:
   trunk/examples/CatalystAdvent/root/2009/pen/dbic-multicreate.pod
Log:
DBIC MultiCreate article

Added: trunk/examples/CatalystAdvent/root/2009/pen/dbic-multicreate.pod
===================================================================
--- trunk/examples/CatalystAdvent/root/2009/pen/dbic-multicreate.pod	                        (rev 0)
+++ trunk/examples/CatalystAdvent/root/2009/pen/dbic-multicreate.pod	2009-12-17 00:09:21 UTC (rev 12415)
@@ -0,0 +1,261 @@
+=head1 Efficiently create multiple related rows in DBIx::Class
+
+This article will talk about something seemingly simple and boring - 
+inserting new rows into your database. If you frequent 
+C<irc.perl.org#dbix-class> you probably have heard of the misterious 
+MultiCreate. Yet are not entirely sure if it can bring you fame and money. 
+Are you watching closely?
+
+=head2 The Pledge
+
+Let's examine the dreaded Artist/CDs example:
+
+ package MySchema::Artist;
+ 
+ use base 'DBIx::Class::Core';
+
+ __PACKAGE__->table ('artist');
+
+ __PACKAGE__->add_columns (
+   id => {
+     data_type => 'int',
+     is_auto_increment => 1,
+   },
+   name => {
+     data_type => 'varchar',
+   },
+ );
+
+ __PACKAGE__->set_primary_key ('id');
+
+ __PACKAGE__->has_many (cds => 'MySchema::CD', 'artist_id');
+
+and
+
+ package MySchema::CD;
+  
+ use base 'DBIx::Class::Core';
+ 
+ __PACKAGE__->table ('cd');
+
+ __PACKAGE__->add_columns (   
+   id => {
+     data_type => 'int',
+     is_auto_increment => 1,
+   },
+   title => {
+     data_type => 'varchar',
+   },
+   artist_id => {
+     data_type => 'int',
+   },
+ );
+
+ __PACKAGE__->set_primary_key ('id');
+
+ __PACKAGE__->belongs_to (artist => 'MySchema::Artist', 'artist_id');
+
+=head2 The Turn
+
+Say some XML describing a modest CD collection needs parsing and stuffing 
+in the above tables, while preserving relational integrity. The seasoned 
+programmer will sigh, drink some coffee and do:
+
+ for my $chunk (@data) {
+
+   my $artist = $schema->resultset ('Artist')->create ({
+     name => $chunk->artist_name
+   });
+
+   for my $cd_title (@{$chunk->{cds}}) {
+     $schema->resultset ('CD')->create ({
+       title => $cd_title,
+       artist_id => $artist->id,
+     });
+   }
+ }
+
+After the coffee kicks in, perhaps the programmer will remember that he 
+already has declared relationships between Artist and CD. So he will reduce 
+the second inner loop to:
+
+ ...
+   for my $cd_title (@{$chunk->{cds}}) {
+     $artist->create_related ('cds', { title => $cd_title });
+   }
+ ...
+
+This is better, but it suffers from a number of deficiencies:
+
+=over
+
+=item
+
+If the XML contained Tracks of individual CDs we would need a 2nd nested 
+loop, which may result in another loop and so on.
+
+=item
+
+The entire operation probably needs to be wrapped in a transaction - that's 
+yet more boilerplate to cargocult
+
+=item
+
+B<It just doesn't feel like DRY>
+
+=back
+
+So the depressed developer sets out to write even more boring dull code, 
+but...
+
+=head2 The Prestige
+
+... the enlightened coworker arrives, looks at the code and changes it to:
+
+ for my $chunk (@data) {
+   my $artist_with_cds = $schema->resultset ('Artist')->create ($chunk);
+ }
+
+Then looks closer at @data, makes sure it doesn't contain anything 
+L<DBIx::Class> wouldn't know how to handle and changes the entire loop to 
+just:
+
+ $schema->resultset ('Artist')->populate (\@data);
+
+=head2 The Trick Revealed
+
+Just like being able to L<turn ResultSets into nested 
+structures|DBIx::Class::ResultClass::HashRefInflator>, L<DBIx::Class> can 
+turn such structures back to related rows. All you have to do is augment the 
+hashref you normally pass to L<DBIx::Class::ResultSet/create> with a 
+structure representing the related row(s), keyed off the B<relationship 
+name>. The structure will be either another hashref (for single-type 
+relationships i.e. L<belongs_to|DBIx::Class::Relationship/belongs_to>, 
+L<has_one|DBIx::Class::Relationship/has_one>, 
+L<might_have|DBIx::Class::Relationship/might_have>) or an arrayref of 
+hashrefs for L<has_many|DBIx::Class::Relationship/has_many> (relationship of 
+type multi).
+
+When the enlightened coworker did C<< ->create ($chunk) >> he effectively 
+supplied:
+
+ {
+   name => 'somename',
+   cds => [
+     {
+       title => 'cd1',
+     },     
+     {
+       title => 'cd2',
+     },
+     ...
+   ],
+ }
+
+which was properly taken apart and inserted in the appropriate tables using 
+the appropriate foreign keys as one would expect. In addition since 
+L<create()|DBIx::Class::ResultSet/create> is a single call, L<DBIx::Class> 
+takes care of making it atomic by dutifully wrapping the entire operation in 
+a L<transaction|DBIx::Class::Manual::Cookbook/TRANSACTIONS>.
+
+The same logic applies to L<DBIx::Class::ResultSet/populate> which happily 
+accepts an arrayref of hashrefs as the creation argument. In fact you can 
+bootstrap some poor-mans replication by simply dumping a 
+L<ResultSet|DBIx::Class::ResultSet> via 
+L<DBIx::Class::ResultClass::HashRefInflator>, and feeding the result straight 
+back to L<create()|DBIx::Class::ResultSet/create>, but on a 
+L<ResultSet|DBIx::Class::ResultSet> derived from the target 
+L<Schema|DBIx::Class::Schema/resultset>.
+
+=head2 But does it blen... scale?
+
+The example above is rather simple, but what if we have a complex chain of 
+relationships? For example:
+
+ Artist->has_many ('cds', ...)
+ CD->has_many ('cd2producer', ...)
+ CD2Producer->belongs_to ('producer', ...)
+ ...and so on
+
+Won't L<DBIx::Class> attempt to create the same producer several times? 
+Fortunately no: any creation attempt over a 
+L<belongs_to|DBIx::Class::Relationship/belongs_to> relationship will be 
+executed via a L<DBIx::Class::ResultSet/find_or_create>, thus allowing 
+everything to just work. For the brave there is an L<example pushing all 
+conceivable 
+limits|http://cpansearch.perl.org/src/FREW/DBIx-Class-0.08115/t/multi_create/torture.t>.
+
+=head2 But there got to be *some* caveats...?
+
+=over
+
+=item
+
+Currently this works for L<create()|DBIx::Class::ResultSet/create> only. 
+While L<find_or_create|DBIx::Class::ResultSet/find_or_create> will be 
+executed on L<belongs_to|DBIx::Class::Relationship/belongs_to> relationships, 
+the final goal is nothing more than new row creation. This means that e.g. 
+passing a nested structure to L<DBIx::Class::ResultSet/update_or_create> will 
+not do anything close to what you would expect.
+
+=item
+
+The returned object does not always have the proper related objects inserted 
+at the expected slots (similar to the effect of 
+L<prefetch|DBIx::Class::ResultSet/prefetch>). We need a good amount of tests 
+before we attempt to make this work as expected - patches welcome!
+
+=item
+
+The L<find_or_create|DBIx::Class::ResultSet/find_or_create> use mentioned 
+above can result in some oddness. Consider:
+
+ my $cd_data = {
+   artist_id => $some_artist->id,
+   title => 'my cd',
+   genre => {
+     name => 'vague genre',
+     cds => [
+       {
+         title => 'oddball cd',
+         artist_id => $some_other_artist->id,
+       },
+     ],
+   }
+ };
+ 
+If the I<vague genre> genre already exists, L<DBIx::Class> will B<not> 
+descend to check if the I<oddball cd> is in fact created. This has not yet 
+caused anyone grief, thus there is no motivation for the (non trivial) fix.
+
+=back
+
+=head1 What The Future Holds
+
+While the new row creation is mostly done and over with (in fact it delayed 
+the release of the 0.081xx series by several months), there are more ways to 
+expand the existing codebase.
+
+=over
+
+=item
+
+Port L<DBIx::Class::ResultSet::RecursiveUpdate> to leverage the (substantial) 
+infrastructure facilitating MultiCreate. While some conceptual differences 
+exist between both modules, they can certainly be resolved during the 
+test-writing phase.
+
+=item
+
+Merge create and update into the logical multi_update_or_create !
+
+=back
+
+If you have tuits or simply ideas - you are always welcome to hang out on 
+IRC, and start a discussion (or snatch a commit-bit and start hacking)
+
+Happy search()ing!
+
+=head1 Author
+
+Peter Rabbitson <ribasushi at cpan.org>




More information about the Catalyst-commits mailing list