X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FResultSet.pm;h=4943d5a3ccfc564010daf71d17416dec04027d39;hb=c817eca810e0239e0a603bddefa8257bd2e914a7;hp=13cfa03a46ee00101fc68757a9747d8a6bb1bc69;hpb=fcf32d04540e2c67625641b0bc004111a7d90252;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/ResultSet.pm b/lib/DBIx/Class/ResultSet.pm index 13cfa03..4943d5a 100644 --- a/lib/DBIx/Class/ResultSet.pm +++ b/lib/DBIx/Class/ResultSet.pm @@ -137,11 +137,15 @@ another. =head3 Resolving conditions and attributes -When a resultset is chained from another resultset, conditions and -attributes with the same keys need resolving. +When a resultset is chained from another resultset (ie: +Csearch(\%extra_cond, \%attrs)>), conditions +and attributes with the same keys need resolving. -L, L, L, L attributes are merged -into the existing ones from the original resultset. +If any of L, L, L are present, they reset the +original selection, and start the selection "clean". + +The L, L, L, L, L attributes +are merged into the existing ones from the original resultset. The L and L attributes, and any search conditions, are merged with an SQL C to the existing condition from the original @@ -1268,23 +1272,28 @@ sub _construct_objects { my $rsrc = $self->result_source; my $attrs = $self->_resolved_attrs; + + if (!$fetch_all and ! $attrs->{order_by} and $attrs->{collapse}) { + # default order for collapsing unless the user asked for something + $attrs->{order_by} = [ map { join '.', $attrs->{alias}, $_} $rsrc->primary_columns ]; + $attrs->{_ordered_for_collapse} = 1; + $attrs->{_order_is_artificial} = 1; + } + my $cursor = $self->cursor; # this will be used as both initial raw-row collector AND as a RV of # _construct_objects. Not regrowing the array twice matters a lot... # a suprising amount actually - my $rows = (delete $self->{stashed_rows}) || []; + my $rows = delete $self->{stashed_rows}; + if ($fetch_all) { # FIXME SUBOPTIMAL - we can do better, cursor->next/all (well diff. methods) should return a ref - $rows = [ @$rows, $cursor->all ]; + $rows = [ ($rows ? @$rows : ()), $cursor->all ]; } - elsif (!$attrs->{collapse}) { - # FIXME SUBOPTIMAL - we can do better, cursor->next/all (well diff. methods) should return a ref - push @$rows, do { my @r = $cursor->next; @r ? \@r : () } - unless @$rows; - } - else { - $attrs->{_ordered_for_collapse} ||= (!$attrs->{order_by}) ? undef : do { + elsif( $attrs->{collapse} ) { + + $attrs->{_ordered_for_collapse} = (!$attrs->{order_by}) ? 0 : do { my $st = $rsrc->schema->storage; my @ord_cols = map { $_->[0] } @@ -1311,68 +1320,128 @@ sub _construct_objects { { $colinfos->{$_}{-colname} => $colinfos->{$_} } @ord_cols })) ? 1 : 0; - }; + } unless defined $attrs->{_ordered_for_collapse}; - if ($attrs->{_ordered_for_collapse}) { - push @$rows, do { my @r = $cursor->next; @r ? \@r : () }; - } - # instead of looping over ->next, use ->all in stealth mode - # *without* calling a ->reset afterwards - # FIXME - encapsulation breach, got to be a better way - elsif (! $cursor->{_done}) { - push @$rows, $cursor->all; - $cursor->{_done} = 1; + if (! $attrs->{_ordered_for_collapse}) { $fetch_all = 1; + + # instead of looping over ->next, use ->all in stealth mode + # *without* calling a ->reset afterwards + # FIXME - encapsulation breach, got to be a better way + if (! $cursor->{_done}) { + $rows = [ ($rows ? @$rows : ()), $cursor->all ]; + $cursor->{_done} = 1; + } } } - return undef unless @$rows; + if (! $fetch_all and ! @{$rows||[]} ) { + # FIXME SUBOPTIMAL - we can do better, cursor->next/all (well diff. methods) should return a ref + if (scalar (my @r = $cursor->next) ) { + $rows = [ \@r ]; + } + } + + return undef unless @{$rows||[]}; + + my @extra_collapser_args; + if ($attrs->{collapse} and ! $fetch_all ) { + + @extra_collapser_args = ( + # FIXME SUBOPTIMAL - we can do better, cursor->next/all (well diff. methods) should return a ref + sub { my @r = $cursor->next or return; \@r }, # how the collapser gets more rows + ($self->{stashed_rows} = []), # where does it stuff excess + ); + } - my $res_class = $self->result_class; - my $inflator = $res_class->can ('inflate_result') - or $self->throw_exception("Inflator $res_class does not provide an inflate_result() method"); + # hotspot - skip the setter + my $res_class = $self->_result_class; + + my $inflator_cref = $self->{_result_inflator}{cref} ||= do { + $res_class->can ('inflate_result') + or $self->throw_exception("Inflator $res_class does not provide an inflate_result() method"); + }; my $infmap = $attrs->{as}; - if (!$attrs->{collapse} and $attrs->{_single_object_inflation}) { - # construct a much simpler array->hash folder for the one-table cases right here + $self->{_result_inflator}{is_hri} = do { ( $inflator_cref == ( + require DBIx::Class::ResultClass::HashRefInflator + && + DBIx::Class::ResultClass::HashRefInflator->can('inflate_result') + ) ) ? 1 : 0 + } unless defined $self->{_result_inflator}{is_hri}; + if ($attrs->{_single_resultclass_inflation}) { + # construct a much simpler array->hash folder for the one-table cases right here + if ($self->{_result_inflator}{is_hri}) { + for my $r (@$rows) { + $r = { map { $infmap->[$_] => $r->[$_] } 0..$#$infmap }; + } + } # FIXME SUBOPTIMAL this is a very very very hot spot # while rather optimal we can *still* do much better, by - # building a smarter [Row|HRI]::inflate_result(), and + # building a smarter Row::inflate_result(), and # switch to feeding it data via a much leaner interface # # crude unscientific benchmarking indicated the shortcut eval is not worth it for # this particular resultset size - if (@$rows < 60) { - my @as_idx = 0..$#$infmap; + elsif (@$rows < 60) { for my $r (@$rows) { - $r = $inflator->($res_class, $rsrc, { map { $infmap->[$_] => $r->[$_] } @as_idx } ); + $r = $inflator_cref->($res_class, $rsrc, { map { $infmap->[$_] => $r->[$_] } (0..$#$infmap) } ); } } else { eval sprintf ( - '$_ = $inflator->($res_class, $rsrc, { %s }) for @$rows', + '$_ = $inflator_cref->($res_class, $rsrc, { %s }) for @$rows', join (', ', map { "\$infmap->[$_] => \$_->[$_]" } 0..$#$infmap ) ); } } - else { - $self->{_row_parser} ||= eval sprintf 'sub { %s }', $rsrc->_mk_row_parser({ + # Special-case multi-object HRI (we always prune) + elsif ($self->{_result_inflator}{is_hri}) { + ( $self->{_row_parser}{hri} ||= $rsrc->_mk_row_parser({ + eval => 1, inflate_map => $infmap, selection => $attrs->{select}, collapse => $attrs->{collapse}, premultiplied => $attrs->{_main_source_premultiplied}, - }) or die $@; + hri_style => 1, + prune_null_branches => 1, + }) )->($rows, @extra_collapser_args); + } + # Regular multi-object + else { - # modify $rows in-place, shrinking/extending as necessary - $self->{_row_parser}->($rows, $fetch_all ? () : ( - # FIXME SUBOPTIMAL - we can do better, cursor->next/all (well diff. methods) should return a ref - sub { my @r = $cursor->next or return; \@r }, # how the collapser gets more rows - ($self->{stashed_rows} = []), # where does it stuff excess - )); + # The rationale is - if this is the ::Row inflator itself, or an around() + # we do prune, because we expect it. + # If not the case - let the user deal with the full output themselves + # Warn them while we are at it so we get a better idea what is out there + # on the DarkPan + $self->{_result_inflator}{prune_null_branches} = do { + $res_class->isa('DBIx::Class::Row') + } ? 1 : 0 unless defined $self->{_result_inflator}{prune_null_branches}; + + unless ($self->{_result_inflator}{prune_null_branches}) { + carp_once ( + "ResultClass $res_class does not inherit from DBIx::Class::Row and " + . 'therefore its inflate_result() will receive the full prefetched data ' + . 'tree, without any branch definedness checks. This is a compatibility ' + . 'measure which will eventually disappear entirely. Please refer to ' + . 't/resultset/inflate_result_api.t for an exhaustive description of the ' + . 'upcoming changes' + ); + } + + ( $self->{_row_parser}{classic}{$self->{_result_inflator}{prune_null_branches}} ||= $rsrc->_mk_row_parser({ + eval => 1, + inflate_map => $infmap, + selection => $attrs->{select}, + collapse => $attrs->{collapse}, + premultiplied => $attrs->{_main_source_premultiplied}, + prune_null_branches => $self->{_result_inflator}{prune_null_branches}, + }) )->($rows, @extra_collapser_args); - $_ = $inflator->($res_class, $rsrc, @$_) for @$rows; + $_ = $inflator_cref->($res_class, $rsrc, @$_) for @$rows; } # CDBI compat stuff @@ -1420,6 +1489,7 @@ in the original source class will not run. sub result_class { my ($self, $result_class) = @_; if ($result_class) { + unless (ref $result_class) { # don't fire this for an object $self->ensure_class_loaded($result_class); } @@ -1428,6 +1498,8 @@ sub result_class { # permit the user to set result class on one result set only; it only # chains if provided to search() #$self->{attrs}{result_class} = $result_class if ref $self; + + delete $self->{_result_inflator}; } $self->_result_class; } @@ -2988,7 +3060,6 @@ Returns a related resultset for the supplied relationship name. sub related_resultset { my ($self, $rel) = @_; - $self->{related_resultsets} ||= {}; return $self->{related_resultsets}{$rel} ||= do { my $rsrc = $self->result_source; my $rel_info = $rsrc->relationship_info($rel); @@ -3015,13 +3086,13 @@ sub related_resultset { #XXX - temp fix for result_class bug. There likely is a more elegant fix -groditi delete @{$attrs}{qw(result_class alias)}; - my $new_cache; + my $related_cache; if (my $cache = $self->get_cache) { - if ($cache->[0] && $cache->[0]->related_resultset($rel)->get_cache) { - $new_cache = [ map { @{$_->related_resultset($rel)->get_cache||[]} } - @$cache ]; - } + $related_cache = [ map + { @{$_->related_resultset($rel)->get_cache||[]} } + @$cache + ]; } my $rel_source = $rsrc->related_source($rel); @@ -3044,7 +3115,7 @@ sub related_resultset { where => $attrs->{where}, }); }; - $new->set_cache($new_cache) if $new_cache; + $new->set_cache($related_cache) if $related_cache; $new; }; } @@ -3269,6 +3340,40 @@ sub _chain_relationship { return {%$attrs, from => $from, seen_join => $seen}; } +# FIXME - this needs to go live in Schema with the tree walker... or +# something +my $inflatemap_checker; +$inflatemap_checker = sub { + my ($rsrc, $relpaths) = @_; + + my $rels; + + for (@$relpaths) { + $_ =~ /^ ( [^\.]+ ) \. (.+) $/x + or next; + + push @{$rels->{$1}}, $2; + } + + for my $rel (keys %$rels) { + my $rel_rsrc = try { + $rsrc->related_source ($rel) + } catch { + $rsrc->throw_exception(sprintf( + "Inflation into non-existent relationship '%s' of '%s' requested, " + . "check the inflation specification (columns/as) ending in '...%s.%s'", + $rel, + $rsrc->source_name, + $rel, + ( sort { length($a) <=> length ($b) } @{$rels->{$rel}} )[0], + ))}; + + $inflatemap_checker->($rel_rsrc, $rels->{$rel}); + } + + return; +}; + sub _resolved_attrs { my $self = shift; return $self->{_attrs} if $self->{_attrs}; @@ -3340,6 +3445,14 @@ sub _resolved_attrs { } } + # validate the user-supplied 'as' chain + # folks get too confused by the (logical) exception message, need to + # go to some lengths to clarify the text + # + # FIXME - this needs to go live in Schema with the tree walker... or + # something + $inflatemap_checker->($source, \@as); + $attrs->{select} = \@sel; $attrs->{as} = \@as; @@ -3450,7 +3563,7 @@ sub _resolved_attrs { } if ( ! List::Util::first { $_ =~ /\./ } @{$attrs->{as}} ) { - $attrs->{_single_object_inflation} = 1; + $attrs->{_single_resultclass_inflation} = 1; $attrs->{collapse} = 0; } @@ -3503,13 +3616,6 @@ sub _resolved_attrs { } } - if (! $attrs->{order_by} and $attrs->{collapse}) { - # default order for collapsing unless the user asked for something - $attrs->{order_by} = [ map { "$alias.$_" } $source->primary_columns ]; - $attrs->{_ordered_for_collapse} = 1; - $attrs->{_order_is_artificial} = 1; - } - # if both page and offset are specified, produce a combined offset # even though it doesn't make much sense, this is what pre 081xx has # been doing @@ -3731,7 +3837,7 @@ sub STORABLE_freeze { # A cursor in progress can't be serialized (and would make little sense anyway) # the parser can be regenerated (and can't be serialized) - delete @{$to_serialize}{qw/cursor _row_parser/}; + delete @{$to_serialize}{qw/cursor _row_parser _result_inflator/}; # nor is it sensical to store a not-yet-fired-count pager if ($to_serialize->{pager} and ref $to_serialize->{pager}{total_entries} eq 'CODE') { @@ -3768,6 +3874,10 @@ sub throw_exception { } } +1; + +__END__ + # XXX: FIXME: Attributes docs need clearing up =head1 ATTRIBUTES @@ -3817,7 +3927,7 @@ syntax as outlined above. =over 4 -=item Value: \@columns +=item Value: \@columns | \%columns | $column =back @@ -3919,14 +4029,6 @@ an explicit list. =back -=head2 +as - -=over 4 - -Indicates additional column names for those added via L. See L. - -=back - =head2 as =over 4 @@ -3969,6 +4071,14 @@ use C instead: You can create your own accessors if required - see L for details. +=head2 +as + +=over 4 + +Indicates additional column names for those added via L. See L. + +=back + =head2 join =over 4 @@ -4032,7 +4142,7 @@ similarly for a third time). For e.g. will return a set of all artists that have both a cd with title 'Down to Earth' and a cd with title 'Popular'. -If you want to fetch related objects from other tables as well, see C +If you want to fetch related objects from other tables as well, see L below. NOTE: An internal join-chain pruner will discard certain joins while @@ -4043,185 +4153,133 @@ below. For more help on using joins with search, see L. -=head2 prefetch +=head2 collapse =over 4 -=item Value: ($rel_name | \@rel_names | \%rel_names) +=item Value: (0 | 1) =back -Contains one or more relationships that should be fetched along with -the main query (when they are accessed afterwards the data will -already be available, without extra queries to the database). This is -useful for when you know you will need the related objects, because it -saves at least one query: - - my $rs = $schema->resultset('Tag')->search( - undef, - { - prefetch => { - cd => 'artist' - } - } - ); - -The initial search results in SQL like the following: - - SELECT tag.*, cd.*, artist.* FROM tag - JOIN cd ON tag.cd = cd.cdid - JOIN artist ON cd.artist = artist.artistid - -L has no need to go back to the database when we access the -C or C relationships, which saves us two SQL statements in this -case. - -Simple prefetches will be joined automatically, so there is no need -for a C attribute in the above search. - -L can be used with the any of the relationship types and -multiple prefetches can be specified together. Below is a more complex -example that prefetches a CD's artist, its liner notes (if present), -the cover image, the tracks on that cd, and the guests on those -tracks. - - # Assuming: - My::Schema::CD->belongs_to( artist => 'My::Schema::Artist' ); - My::Schema::CD->might_have( liner_note => 'My::Schema::LinerNotes' ); - My::Schema::CD->has_one( cover_image => 'My::Schema::Artwork' ); - My::Schema::CD->has_many( tracks => 'My::Schema::Track' ); - - My::Schema::Artist->belongs_to( record_label => 'My::Schema::RecordLabel' ); - - My::Schema::Track->has_many( guests => 'My::Schema::Guest' ); - - - my $rs = $schema->resultset('CD')->search( - undef, - { - prefetch => [ - { artist => 'record_label'}, # belongs_to => belongs_to - 'liner_note', # might_have - 'cover_image', # has_one - { tracks => 'guests' }, # has_many => has_many - ] - } - ); - -This will produce SQL like the following: - - SELECT cd.*, artist.*, record_label.*, liner_note.*, cover_image.*, - tracks.*, guests.* - FROM cd me - JOIN artist artist - ON artist.artistid = me.artistid - JOIN record_label record_label - ON record_label.labelid = artist.labelid - LEFT JOIN track tracks - ON tracks.cdid = me.cdid - LEFT JOIN guest guests - ON guests.trackid = track.trackid - LEFT JOIN liner_notes liner_note - ON liner_note.cdid = me.cdid - JOIN cd_artwork cover_image - ON cover_image.cdid = me.cdid - ORDER BY tracks.cd - -Now the C, C, C, C, -C, and C of the CD will all be available through the -relationship accessors without the need for additional queries to the -database. - -However, there is one caveat to be observed: it can be dangerous to -prefetch more than one L -relationship on a given level. e.g.: - - my $rs = $schema->resultset('CD')->search( - undef, - { - prefetch => [ - 'tracks', # has_many - { cd_to_producer => 'producer' }, # has_many => belongs_to (i.e. m2m) - ] - } - ); - -The collapser currently can't identify duplicate tuples for multiple -L relationships and as a -result the second L -relation could contain redundant objects. +When set to a true value, indicates that any rows fetched from joined has_many +relationships are to be aggregated into the corresponding "parent" object. For +example, the resultset: -=head3 Using L with L + my $rs = $schema->resultset('CD')->search({}, { + '+columns' => [ qw/ tracks.title tracks.position / ], + join => 'tracks', + collapse => 1, + }); -L implies a L with the equivalent argument, and is -properly merged with any existing L specification. So the -following: +While executing the following query: - my $rs = $schema->resultset('CD')->search( - {'record_label.name' => 'Music Product Ltd.'}, - { - join => {artist => 'record_label'}, - prefetch => 'artist', - } - ); + SELECT me.*, tracks.title, tracks.position + FROM cd me + LEFT JOIN track tracks + ON tracks.cdid = me.cdid -... will work, searching on the record label's name, but only -prefetching the C. +Will return only as many objects as there are rows in the CD source, even +though the result of the query may span many rows. Each of these CD objects +will in turn have multiple "Track" objects hidden behind the has_many +generated accessor C. Without C<< collapse => 1 >>, the return values +of this resultset would be as many CD objects as there are tracks (a "Cartesian +product"), with each CD object containing exactly one of all fetched Track data. -=head3 Using L with L / L / L / L +When a collapse is requested on a non-ordered resultset, an order by some +unique part of the main source (the left-most table) is inserted automatically. +This is done so that the resultset is allowed to be "lazy" - calling +L<< $rs->next|/next >> will fetch only as many rows as it needs to build the next +object with all of its related data. -L implies a L/L with the fields of the -prefetched relations. So given: +If an L is already declared, and orders the resultset in a way that +makes collapsing as described above impossible (e.g. C<< ORDER BY +has_many_rel.column >> or C), DBIC will automatically +switch to "eager" mode and slurp the entire resultset before consturcting the +first object returned by L. - my $rs = $schema->resultset('CD')->search( - undef, - { - select => ['cd.title'], - as => ['cd_title'], - prefetch => 'artist', - } - ); +Setting this attribute on a resultset that does not join any has_many +relations is a no-op. -The L becomes: C<'cd.title', 'artist.*'> and the L -becomes: C<'cd_title', 'artist.*'>. +For a more in-depth discussion, see L. -=head3 CAVEATS - -Prefetch does a lot of deep magic. As such, it may not behave exactly -as you might expect. +=head2 prefetch =over 4 -=item * - -Prefetch uses the L to populate the prefetched relationships. This -may or may not be what you want. +=item Value: ($rel_name | \@rel_names | \%rel_names) -=item * +=back -If you specify a condition on a prefetched relationship, ONLY those -rows that match the prefetched condition will be fetched into that relationship. -This means that adding prefetch to a search() B what is returned by -traversing a relationship. So, if you have C<< Artist->has_many(CDs) >> and you do +This attribute is a shorthand for specifying a L spec, adding all +columns from the joined related sources as L and setting +L to a true value. For example, the following two queries are +equivalent: - my $artist_rs = $schema->resultset('Artist')->search({ - 'cds.year' => 2008, - }, { - join => 'cds', + my $rs = $schema->resultset('Artist')->search({}, { + prefetch => { cds => ['genre', 'tracks' ] }, }); - my $count = $artist_rs->first->cds->count; +and - my $artist_rs_prefetch = $artist_rs->search( {}, { prefetch => 'cds' } ); + my $rs = $schema->resultset('Artist')->search({}, { + join => { cds => ['genre', 'tracks' ] }, + collapse => 1, + '+columns' => [ + (map + { +{ "cds.$_" => "cds.$_" } } + $schema->source('Artist')->related_source('cds')->columns + ), + (map + { +{ "cds.genre.$_" => "genre.$_" } } + $schema->source('Artist')->related_source('cds')->related_source('genre')->columns + ), + (map + { +{ "cds.tracks.$_" => "tracks.$_" } } + $schema->source('Artist')->related_source('cds')->related_source('tracks')->columns + ), + ], + }); - my $prefetch_count = $artist_rs_prefetch->first->cds->count; +Both producing the following SQL: + + SELECT me.artistid, me.name, me.rank, me.charfield, + cds.cdid, cds.artist, cds.title, cds.year, cds.genreid, cds.single_track, + genre.genreid, genre.name, + tracks.trackid, tracks.cd, tracks.position, tracks.title, tracks.last_updated_on, tracks.last_updated_at + FROM artist me + LEFT JOIN cd cds + ON cds.artist = me.artistid + LEFT JOIN genre genre + ON genre.genreid = cds.genreid + LEFT JOIN track tracks + ON tracks.cd = cds.cdid + ORDER BY me.artistid + +While L implies a L, it is ok to mix the two together, as +the arguments are properly merged and generally do the right thing. For +example, you may want to do the following: + + my $artists_and_cds_without_genre = $schema->resultset('Artist')->search( + { 'genre.genreid' => undef }, + { + join => { cds => 'genre' }, + prefetch => 'cds', + } + ); - cmp_ok( $count, '==', $prefetch_count, "Counts should be the same" ); +Which generates the following SQL: -that cmp_ok() may or may not pass depending on the datasets involved. This -behavior may or may not survive the 0.09 transition. + SELECT me.artistid, me.name, me.rank, me.charfield, + cds.cdid, cds.artist, cds.title, cds.year, cds.genreid, cds.single_track + FROM artist me + LEFT JOIN cd cds + ON cds.artist = me.artistid + LEFT JOIN genre genre + ON genre.genreid = cds.genreid + WHERE genre.genreid IS NULL + ORDER BY me.artistid -=back +For a more in-depth discussion, see L. =head2 alias @@ -4399,6 +4457,128 @@ Set to 'update' for a SELECT ... FOR UPDATE or 'shared' for a SELECT ... FOR SHARED. If \$scalar is passed, this is taken directly and embedded in the query. +=head1 PREFETCHING + +DBIx::Class supports arbitrary related data prefetching from multiple related +sources. Any combination of relationship types and column sets are supported. +If L is requested, there is an additional requirement of +selecting enough data to make every individual object uniquely identifiable. + +Here are some more involved examples, based on the following relationship map: + + # Assuming: + My::Schema::CD->belongs_to( artist => 'My::Schema::Artist' ); + My::Schema::CD->might_have( liner_note => 'My::Schema::LinerNotes' ); + My::Schema::CD->has_many( tracks => 'My::Schema::Track' ); + + My::Schema::Artist->belongs_to( record_label => 'My::Schema::RecordLabel' ); + + My::Schema::Track->has_many( guests => 'My::Schema::Guest' ); + + + + my $rs = $schema->resultset('Tag')->search( + undef, + { + prefetch => { + cd => 'artist' + } + } + ); + +The initial search results in SQL like the following: + + SELECT tag.*, cd.*, artist.* FROM tag + JOIN cd ON tag.cd = cd.cdid + JOIN artist ON cd.artist = artist.artistid + +L has no need to go back to the database when we access the +C or C relationships, which saves us two SQL statements in this +case. + +Simple prefetches will be joined automatically, so there is no need +for a C attribute in the above search. + +The L attribute can be used with any of the relationship types +and multiple prefetches can be specified together. Below is a more complex +example that prefetches a CD's artist, its liner notes (if present), +the cover image, the tracks on that CD, and the guests on those +tracks. + + my $rs = $schema->resultset('CD')->search( + undef, + { + prefetch => [ + { artist => 'record_label'}, # belongs_to => belongs_to + 'liner_note', # might_have + 'cover_image', # has_one + { tracks => 'guests' }, # has_many => has_many + ] + } + ); + +This will produce SQL like the following: + + SELECT cd.*, artist.*, record_label.*, liner_note.*, cover_image.*, + tracks.*, guests.* + FROM cd me + JOIN artist artist + ON artist.artistid = me.artistid + JOIN record_label record_label + ON record_label.labelid = artist.labelid + LEFT JOIN track tracks + ON tracks.cdid = me.cdid + LEFT JOIN guest guests + ON guests.trackid = track.trackid + LEFT JOIN liner_notes liner_note + ON liner_note.cdid = me.cdid + JOIN cd_artwork cover_image + ON cover_image.cdid = me.cdid + ORDER BY tracks.cd + +Now the C, C, C, C, +C, and C of the CD will all be available through the +relationship accessors without the need for additional queries to the +database. + +=head3 CAVEATS + +Prefetch does a lot of deep magic. As such, it may not behave exactly +as you might expect. + +=over 4 + +=item * + +Prefetch uses the L to populate the prefetched relationships. This +may or may not be what you want. + +=item * + +If you specify a condition on a prefetched relationship, ONLY those +rows that match the prefetched condition will be fetched into that relationship. +This means that adding prefetch to a search() B what is returned by +traversing a relationship. So, if you have C<< Artist->has_many(CDs) >> and you do + + my $artist_rs = $schema->resultset('Artist')->search({ + 'cds.year' => 2008, + }, { + join => 'cds', + }); + + my $count = $artist_rs->first->cds->count; + + my $artist_rs_prefetch = $artist_rs->search( {}, { prefetch => 'cds' } ); + + my $prefetch_count = $artist_rs_prefetch->first->cds->count; + + cmp_ok( $count, '==', $prefetch_count, "Counts should be the same" ); + +That cmp_ok() may or may not pass depending on the datasets involved. This +behavior may or may not survive the 0.09 transition. + +=back + =head1 DBIC BIND VALUES Because DBIC may need more information to bind values than just the column name @@ -4455,6 +4635,3 @@ See L and L in You may distribute this code under the same terms as Perl itself. -=cut - -1;