[Catalyst-commits] r12318 - in
trunk/examples/CatalystAdvent/root/2009: . pen
zamolxes at dev.catalyst.perl.org
zamolxes at dev.catalyst.perl.org
Sat Dec 12 00:04:36 GMT 2009
Author: zamolxes
Date: 2009-12-12 00:04:36 +0000 (Sat, 12 Dec 2009)
New Revision: 12318
Added:
trunk/examples/CatalystAdvent/root/2009/12.pod
Removed:
trunk/examples/CatalystAdvent/root/2009/pen/withmetadata.pod
Log:
day 12
Copied: trunk/examples/CatalystAdvent/root/2009/12.pod (from rev 12317, trunk/examples/CatalystAdvent/root/2009/pen/withmetadata.pod)
===================================================================
--- trunk/examples/CatalystAdvent/root/2009/12.pod (rev 0)
+++ trunk/examples/CatalystAdvent/root/2009/12.pod 2009-12-12 00:04:36 UTC (rev 12318)
@@ -0,0 +1,285 @@
+=head1 How DBIx::Class::ResultSet::WithMetaData can help keep your controllers clean
+
+=head2 A little note on code cleanliness
+
+When you started using Catalyst with C<DBIx::Class> I'm betting that you
+generated a resultset in your controller, then passed it to your view
+(TT for example), then iterated through it. In short you ended up doing
+loads of really complicated stuff in your TT templates. As you probably
+learned, this is really bad, because you eventually end up with
+complicated and messy templates that are hard to maintain. You should be
+doing your data preparation in Perl, then passing some nicely formatted
+data structure to your view to happily render with a minimum of logic.
+
+A nice approach is to harness the resultset chaining magic that
+C<DBIx::Class> provides to build up your resultset in reusable stages,
+so first you add some data:
+
+ my $new_rs = $rs->search({}, { prefetch => [qw/artist/] });
+
+And then restrict it a bit:
+
+ my $newer_rs = $new_rs->search({ price => { '>' => 6 } });
+
+And then use C<DBIx::Class::ResultClass::HashRefInflator> to dump the
+resultset to an array of hashrefs and put that in your stash. The data
+could look something like this:
+
+ $newer_rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
+ my @tunes = $newer_rs->all;
+
+ # [{
+ # 'id' => 1,
+ # 'name' => 'Catchy tune',
+ # 'price' => '7',
+ # 'artist' => {
+ # 'name' => 'Some dude'
+ # }
+ # },
+ # {
+ # 'id' => 2,
+ # 'name' => 'Not so catchy tune',
+ # 'price' => '7.5'
+ # 'artist' => {
+ # 'name' => 'Some other dude'
+ # }
+ # }]
+
+ $c->stash->{tunes} = \@tunes;
+
+And then your view can just iterate through the data structure without
+having to deal with row objects or pull in more data from the
+database. And that's really great. It's much better to pass an already
+formatted data structure to your view then it is to pass a resultset to
+your view, because application will be much more maintainable if you're
+doing all the data processing in Perl.
+
+This approach of building up the resultset in stages is great if you're
+just prefetching, or otherwise adding data that's in the database, but
+what if you need to add some stuff to the data structure that isn't in
+the database? Probably you'll end up doing something like this:
+
+ $rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
+ my @tunes = $rs->all;
+
+ foreach my $tune (@tunes) {
+ $tune->{score} = $score_map{ $tune->{id} };
+ # and so on, adding more stuff to the row's hashref
+ }
+
+Typically you'll do that in the controller, which is bad, because you
+should be keeping your logic in the model. When you realise this is bad
+you'll move it to the model; maybe you'll make a resultset method and
+call it from your controller like this:
+
+ my $formatted_tunes = $rs->get_formatted_arrayref;
+ $c->stash->{tunes} = $formatted_tunes;
+
+Which is sort of better, until you realise that in another action you
+need to reuse this method from somewhere else in your application, but
+this time you need more stuff, so maybe you extend your method like
+this:
+
+ my $formatted_tunes_with_extra_stuff =
+ $rs->get_formatted_arrayref( with_extra_stuff => 1 );
+ $c->stash->{tunes} = $formatted_tunes_with_extra_stuff;
+
+And then in another action you realise you need the same thing, but with
+a different scoring mechanism:
+
+ my $formatted_tunes_with_extra_stuff_and_a different_scoring_mechanism =
+ $rs->get_formatted_arrayref( with_extra_stuff => 1, scoring => 'different' );
+
+And soon your C<get_formatted_arrayref> method is unmaintainable. I
+thought that it would be cool if I could just add this extra stuff by
+chaining resultsets together:
+
+ my $formatted_tunes = $rs->with_score->display;
+
+ my $formatted_tunes_with_extra_stuff =
+ $rs->with_score->with_extra_stuff->display;
+
+ my $formatted_tunes_with_extra_stuff_and_a different_scoring_mechanism =
+ $rs->with_score( mechanism => 'different' )->with_extra_stuff->display;
+
+And until you call display on it, it's still just a resultset, with the
+usual resultset methods:
+
+ my $formatted_tunes = $rs->with_score
+ ->with_extra_stuff
+ ->search({}, { prefetch => 'artist' })
+ ->display;
+
+C<DBIx::Class::ResultSet::WithMetaData> allows you to do this - you can
+attach extra metadata to your resultset without first flattening it to
+a data structure, which will allow you to separate your formatting out to
+separate methods in a relatively clean way that promotes reuse.
+
+=head2 Whoa there, how do I add my own resultset methods?
+
+You need to use a custom resultset, which is just a subclass of the
+usual C<DBIx::Class::ResultSet>. There are two ways to add custom
+resultsets. Preferably, you'll use load_namespaces in your
+C<DBIx::Class::Schema class>, like this:
+
+ package MyApp::Schema;
+
+ ...
+
+ __PACKAGE__->load_namespaces(
+ result_namespace => 'Result',
+ resultset_namespace => 'ResultSet',
+ );
+
+In which case your custom resultsets will be automatically picked up from
+MyApp::Schema::ResultSet::*.
+
+If you're not using load_namespaces, you can still make it work. Have a
+look at this:
+http://search.cpan.org/~frew/DBIx-Class-0.08114/lib/DBIx/Class/ResultSource.pm#resultset_class
+
+And a super simple resultset class might look like this:
+
+ package MyApp::Schema::ResultSet::Tune;
+
+ use strict;
+ use warnings;
+
+ use DBIx::Class::ResultClass::HashRefInflator;
+ use Moose;
+ extends 'DBIx::Class::ResultSet';
+
+ sub display {
+ my ($self) = @_;
+
+ $rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
+ my @return = $rs->all;
+ return \@return;
+ }
+
+ 1;
+
+Which provides a display method on your Tune resultsets, like so:
+
+ my $displayed = $schema->resultset('Tune')->display;
+
+=head2 So down to business, what does
+DBIx::Class::ResultSet::WithMetaData actually provide?
+
+The key method is called C<add_row_info>, which allows you to attach the
+extra metadata to the rows. For example, in the first section we were
+adding a score to each row. You could do that like this:
+
+ package MyApp::Schema::ResultSet::Tune;
+
+ ...
+
+ extends 'DBIx::Class::ResultSet::WithMetaData';
+
+ sub with_score {
+ my ($self, %params) = @_;
+
+ foreach my $row ($self->all) {
+ my $score = $self->score_map->{ $row->id };
+ $self->add_row_info(id => $row->id, info => { score => $score });
+ }
+ }
+
+ ...
+
+The only other method you need to worry about is the display method,
+which flattens the resultset to a data structure, much like using
+C<DBIx::Class::ResultClass::HashRefInflator>. But it also merges the
+extra info attached using add_row_info. For example
+
+ my @tunes = $tune_rs->with_score->display;
+
+ # [{
+ # 'id' => 1,
+ # 'name' => 'Catchy tune',
+ # 'price' => '7',
+ # 'score' => '1.7',
+ # },
+ # {
+ # 'id' => 2,
+ # 'name' => 'Not so catchy tune',
+ # 'price' => '7.5'
+ # 'score' => '1.2'
+ # }]
+
+=head2 So how does this keep my code clean again?
+
+These are simple examples. If you have a fairly complex application
+(like just about any real production application) then you might be
+building up complex chains to format your data ready for the
+view. You'll be add adding all sorts of extra stuff. Look at this
+example I've taken from one of my apps:
+
+ $c->stash->{products} = $rs->with_images
+ ->with_primary_image
+ ->with_seller
+ ->with_primary_category
+ ->with_related_products
+ ->with_options
+ ->with_token
+ ->display;
+
+In another part of the application I need the products but with less
+info, but I can just easily reuse what I already have:
+
+ $c->stash->{products} = $rs->with_images
+ ->with_primary_image
+ ->with_seller
+ ->with_token
+ ->display;
+
+This might look complicated, but it's quite elegant when compared with
+writing one huge method which accepts a ton of flags determining whether
+or not to include different bits of info to the data structure, or
+writing separate methods for each use case, or doing it in the templates,
+or doing it in the controller.
+
+To summarize:
+
+=over 4
+
+=item *
+
+you're not doing your data pre-processing in the view where it doesn't
+belong
+
+=item *
+
+you're not doing it in the controller where it doesn't belong either
+
+=item *
+
+you've split up your formatting into reusable methods inside the model
+
+=item *
+
+you'll sleep well at night and your colleagues won't hate you do much
+
+=back
+
+=head2 It's not very efficient though is it?
+
+It's not really, no. You'll find that you're looping through the
+resultset repeatedly in order to format it, and if you're working with
+large resultsets, this might not be for you. But my general attitude is
+that you should make the code work in a maintainable and clean way, and
+then optimize for performance. Don't start coding yourself into a mess
+before you know the clean alternatives are slow.
+
+=head2 And it's fairly experimental.
+
+Although used in production on a couple of my applications, it's still
+newish and kind of experimental. But it's simple enough so improvements
+and optimizations shouldn't hard to make, and if you want to help
+improve things, I'm liberal with commit bits and co-maint rights.
+
+=head1 AUTHOR
+
+Luke Saunders <luke.saunders at gmail.com>
+
+=cut
Deleted: trunk/examples/CatalystAdvent/root/2009/pen/withmetadata.pod
===================================================================
--- trunk/examples/CatalystAdvent/root/2009/pen/withmetadata.pod 2009-12-11 23:28:09 UTC (rev 12317)
+++ trunk/examples/CatalystAdvent/root/2009/pen/withmetadata.pod 2009-12-12 00:04:36 UTC (rev 12318)
@@ -1,285 +0,0 @@
-=head1 How DBIx::Class::ResultSet::WithMetaData can help keep your controllers clean
-
-=head2 A little note on code cleanliness
-
-When you started using Catalyst with C<DBIx::Class> I'm betting that you
-generated a resultset in your controller, then passed it to your view
-(TT for example), then iterated through it. In short you ended up doing
-loads of really complicated stuff in your TT templates. As you probably
-learned, this is really bad, because you eventually end up with
-complicated and messy templates that are hard to maintain. You should be
-doing your data preparation in Perl, then passing some nicely formatted
-data structure to your view to happily render with a minimum of logic.
-
-A nice approach is to harness the resultset chaining magic that
-C<DBIx::Class> provides to build up your resultset in reusable stages,
-so first you add some data:
-
- my $new_rs = $rs->search({}, { prefetch => [qw/artist/] });
-
-And then restrict it a bit:
-
- my $newer_rs = $new_rs->search({ price => { '>' => 6 } });
-
-And then use C<DBIx::Class::ResultClass::HashRefInflator> to dump the
-resultset to an array of hashrefs and put that in your stash. The data
-could look something like this:
-
- $newer_rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
- my @tunes = $newer_rs->all;
-
- # [{
- # 'id' => 1,
- # 'name' => 'Catchy tune',
- # 'price' => '7',
- # 'artist' => {
- # 'name' => 'Some dude'
- # }
- # },
- # {
- # 'id' => 2,
- # 'name' => 'Not so catchy tune',
- # 'price' => '7.5'
- # 'artist' => {
- # 'name' => 'Some other dude'
- # }
- # }]
-
- $c->stash->{tunes} = \@tunes;
-
-And then your view can just iterate through the data structure without
-having to deal with row objects or pull in more data from the
-database. And that's really great. It's much better to pass an already
-formatted data structure to your view then it is to pass a resultset to
-your view, because application will be much more maintainable if you're
-doing all the data processing in Perl.
-
-This approach of building up the resultset in stages is great if you're
-just prefetching, or otherwise adding data that's in the database, but
-what if you need to add some stuff to the data structure that isn't in
-the database? Probably you'll end up doing something like this:
-
- $rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
- my @tunes = $rs->all;
-
- foreach my $tune (@tunes) {
- $tune->{score} = $score_map{ $tune->{id} };
- # and so on, adding more stuff to the row's hashref
- }
-
-Typically you'll do that in the controller, which is bad, because you
-should be keeping your logic in the model. When you realise this is bad
-you'll move it to the model; maybe you'll make a resultset method and
-call it from your controller like this:
-
- my $formatted_tunes = $rs->get_formatted_arrayref;
- $c->stash->{tunes} = $formatted_tunes;
-
-Which is sort of better, until you realise that in another action you
-need to reuse this method from somewhere else in your application, but
-this time you need more stuff, so maybe you extend your method like
-this:
-
- my $formatted_tunes_with_extra_stuff =
- $rs->get_formatted_arrayref( with_extra_stuff => 1 );
- $c->stash->{tunes} = $formatted_tunes_with_extra_stuff;
-
-And then in another action you realise you need the same thing, but with
-a different scoring mechanism:
-
- my $formatted_tunes_with_extra_stuff_and_a different_scoring_mechanism =
- $rs->get_formatted_arrayref( with_extra_stuff => 1, scoring => 'different' );
-
-And soon your C<get_formatted_arrayref> method is unmaintainable. I
-thought that it would be cool if I could just add this extra stuff by
-chaining resultsets together:
-
- my $formatted_tunes = $rs->with_score->display;
-
- my $formatted_tunes_with_extra_stuff =
- $rs->with_score->with_extra_stuff->display;
-
- my $formatted_tunes_with_extra_stuff_and_a different_scoring_mechanism =
- $rs->with_score( mechanism => 'different' )->with_extra_stuff->display;
-
-And until you call display on it, it's still just a resultset, with the
-usual resultset methods:
-
- my $formatted_tunes = $rs->with_score
- ->with_extra_stuff
- ->search({}, { prefetch => 'artist' })
- ->display;
-
-C<DBIx::Class::ResultSet::WithMetaData> allows you to do this - you can
-attach extra metadata to your resultset without first flattening it to
-a data structure, which will allow you to separate your formatting out to
-separate methods in a relatively clean way that promotes reuse.
-
-=head2 Whoa there, how do I add my own resultset methods?
-
-You need to use a custom resultset, which is just a subclass of the
-usual C<DBIx::Class::ResultSet>. There are two ways to add custom
-resultsets. Preferably, you'll use load_namespaces in your
-C<DBIx::Class::Schema class>, like this:
-
- package MyApp::Schema;
-
- ...
-
- __PACKAGE__->load_namespaces(
- result_namespace => 'Result',
- resultset_namespace => 'ResultSet',
- );
-
-In which case your custom resultsets will be automatically picked up from
-MyApp::Schema::ResultSet::*.
-
-If you're not using load_namespaces, you can still make it work. Have a
-look at this:
-http://search.cpan.org/~frew/DBIx-Class-0.08114/lib/DBIx/Class/ResultSource.pm#resultset_class
-
-And a super simple resultset class might look like this:
-
- package MyApp::Schema::ResultSet::Tune;
-
- use strict;
- use warnings;
-
- use DBIx::Class::ResultClass::HashRefInflator;
- use Moose;
- extends 'DBIx::Class::ResultSet';
-
- sub display {
- my ($self) = @_;
-
- $rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
- my @return = $rs->all;
- return \@return;
- }
-
- 1;
-
-Which provides a display method on your Tune resultsets, like so:
-
- my $displayed = $schema->resultset('Tune')->display;
-
-=head2 So down to business, what does
-DBIx::Class::ResultSet::WithMetaData actually provide?
-
-The key method is called C<add_row_info>, which allows you to attach the
-extra metadata to the rows. For example, in the first section we were
-adding a score to each row. You could do that like this:
-
- package MyApp::Schema::ResultSet::Tune;
-
- ...
-
- extends 'DBIx::Class::ResultSet::WithMetaData';
-
- sub with_score {
- my ($self, %params) = @_;
-
- foreach my $row ($self->all) {
- my $score = $self->score_map->{ $row->id };
- $self->add_row_info(id => $row->id, info => { score => $score });
- }
- }
-
- ...
-
-The only other method you need to worry about is the display method,
-which flattens the resultset to a data structure, much like using
-C<DBIx::Class::ResultClass::HashRefInflator>. But it also merges the
-extra info attached using add_row_info. For example
-
- my @tunes = $tune_rs->with_score->display;
-
- # [{
- # 'id' => 1,
- # 'name' => 'Catchy tune',
- # 'price' => '7',
- # 'score' => '1.7',
- # },
- # {
- # 'id' => 2,
- # 'name' => 'Not so catchy tune',
- # 'price' => '7.5'
- # 'score' => '1.2'
- # }]
-
-=head2 So how does this keep my code clean again?
-
-These are simple examples. If you have a fairly complex application
-(like just about any real production application) then you might be
-building up complex chains to format your data ready for the
-view. You'll be add adding all sorts of extra stuff. Look at this
-example I've taken from one of my apps:
-
- $c->stash->{products} = $rs->with_images
- ->with_primary_image
- ->with_seller
- ->with_primary_category
- ->with_related_products
- ->with_options
- ->with_token
- ->display;
-
-In another part of the application I need the products but with less
-info, but I can just easily reuse what I already have:
-
- $c->stash->{products} = $rs->with_images
- ->with_primary_image
- ->with_seller
- ->with_token
- ->display;
-
-This might look complicated, but it's quite elegant when compared with
-writing one huge method which accepts a ton of flags determining whether
-or not to include different bits of info to the data structure, or
-writing separate methods for each use case, or doing it in the templates,
-or doing it in the controller.
-
-To summarize:
-
-=over 4
-
-=item *
-
-you're not doing your data pre-processing in the view where it doesn't
-belong
-
-=item *
-
-you're not doing it in the controller where it doesn't belong either
-
-=item *
-
-you've split up your formatting into reusable methods inside the model
-
-=item *
-
-you'll sleep well at night and your colleagues won't hate you do much
-
-=back
-
-=head2 It's not very efficient though is it?
-
-It's not really, no. You'll find that you're looping through the
-resultset repeatedly in order to format it, and if you're working with
-large resultsets, this might not be for you. But my general attitude is
-that you should make the code work in a maintainable and clean way, and
-then optimize for performance. Don't start coding yourself into a mess
-before you know the clean alternatives are slow.
-
-=head2 And it's fairly experimental.
-
-Although used in production on a couple of my applications, it's still
-newish and kind of experimental. But it's simple enough so improvements
-and optimizations shouldn't hard to make, and if you want to help
-improve things, I'm liberal with commit bits and co-maint rights.
-
-=head1 AUTHOR
-
-Luke Saunders <luke.saunders at gmail.com>
-
-=cut
More information about the Catalyst-commits
mailing list