X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FResultSet.pm;h=a704b9d0feec47679b35e606882125131b78b6b3;hb=82977fd41d464702b7b1768282afdcebb0f91014;hp=ed5a7aed46432850558d7f620850fe38ec19f0e7;hpb=69bc5f2b82b4a4f027cd9d57c38c25dc4e0b72c0;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/ResultSet.pm b/lib/DBIx/Class/ResultSet.pm index ed5a7ae..a704b9d 100644 --- a/lib/DBIx/Class/ResultSet.pm +++ b/lib/DBIx/Class/ResultSet.pm @@ -4,7 +4,6 @@ use strict; use warnings; use base qw/DBIx::Class/; use DBIx::Class::Carp; -use DBIx::Class::Exception; use DBIx::Class::ResultSetColumn; use Scalar::Util qw/blessed weaken/; use Try::Tiny; @@ -138,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 i.e. +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". + +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 @@ -354,20 +357,36 @@ always return a resultset, even in list context. sub search_rs { my $self = shift; - # Special-case handling for (undef, undef). - if ( @_ == 2 && !defined $_[1] && !defined $_[0] ) { - @_ = (); - } + my $rsrc = $self->result_source; + my ($call_cond, $call_attrs); - my $call_attrs = {}; - if (@_ > 1) { - if (ref $_[-1] eq 'HASH') { - # copy for _normalize_selection - $call_attrs = { %{ pop @_ } }; - } - elsif (! defined $_[-1] ) { - pop @_; # search({}, undef) + # Special-case handling for (undef, undef) or (undef) + # Note that (foo => undef) is valid deprecated syntax + @_ = () if not scalar grep { defined $_ } @_; + + # just a cond + if (@_ == 1) { + $call_cond = shift; + } + # fish out attrs in the ($condref, $attr) case + elsif (@_ == 2 and ( ! defined $_[0] or (ref $_[0]) ne '') ) { + ($call_cond, $call_attrs) = @_; + } + elsif (@_ % 2) { + $self->throw_exception('Odd number of arguments to search') + } + # legacy search + elsif (@_) { + carp_unique 'search( %condition ) is deprecated, use search( \%condition ) instead' + unless $rsrc->result_class->isa('DBIx::Class::CDBICompat'); + + for my $i (0 .. $#_) { + next if $i % 2; + $self->throw_exception ('All keys in condition key/value pairs must be plain scalars') + if (! defined $_[$i] or ref $_[$i] ne ''); } + + $call_cond = { @_ }; } # see if we can keep the cache (no $rs changes) @@ -383,8 +402,6 @@ sub search_rs { $cache = $self->get_cache; } - my $rsrc = $self->result_source; - my $old_attrs = { %{$self->{attrs}} }; my $old_having = delete $old_attrs->{having}; my $old_where = delete $old_attrs->{where}; @@ -392,7 +409,10 @@ sub search_rs { my $new_attrs = { %$old_attrs }; # take care of call attrs (only if anything is changing) - if (keys %$call_attrs) { + if ($call_attrs and keys %$call_attrs) { + + # copy for _normalize_selection + $call_attrs = { %$call_attrs }; my @selector_attrs = qw/select as columns cols +select +as +columns include_columns/; @@ -439,28 +459,6 @@ sub search_rs { } - # rip apart the rest of @_, parse a condition - my $call_cond = do { - - if (ref $_[0] eq 'HASH') { - (keys %{$_[0]}) ? $_[0] : undef - } - elsif (@_ == 1) { - $_[0] - } - elsif (@_ % 2) { - $self->throw_exception('Odd number of arguments to search') - } - else { - +{ @_ } - } - - } if @_; - - if( @_ > 1 and ! $rsrc->result_class->isa('DBIx::Class::CDBICompat') ) { - carp_unique 'search( %condition ) is deprecated, use search( \%condition ) instead'; - } - for ($old_where, $call_cond) { if (defined $_) { $new_attrs->{where} = $self->_stack_cond ( @@ -844,7 +842,7 @@ sub find { # Run the query, passing the result_class since it should propagate for find my $rs = $self->search ($final_cond, {result_class => $self->result_class, %$attrs}); - if (keys %{$rs->_resolved_attrs->{collapse}}) { + if ($rs->_resolved_attrs->{collapse}) { my $row = $rs->next; carp "Query returned more than one row" if $rs->next; return $row; @@ -993,13 +991,14 @@ L for more information. =cut sub cursor { - my ($self) = @_; - - my $attrs = $self->_resolved_attrs_copy; + my $self = shift; - return $self->{cursor} - ||= $self->result_source->storage->select($attrs->{from}, $attrs->{select}, - $attrs->{where},$attrs); + return $self->{cursor} ||= do { + my $attrs = { %{$self->_resolved_attrs } }; + $self->result_source->storage->select( + $attrs->{from}, $attrs->{select}, $attrs->{where}, $attrs + ); + }; } =head2 single @@ -1051,13 +1050,11 @@ sub single { $self->throw_exception('single() only takes search conditions, no attributes. You want ->search( $cond, $attrs )->single()'); } - my $attrs = $self->_resolved_attrs_copy; + my $attrs = { %{$self->_resolved_attrs} }; - if (keys %{$attrs->{collapse}}) { - $self->throw_exception( - 'single() can not be used on resultsets prefetching has_many. Use find( \%cond ) or next() instead' - ); - } + $self->throw_exception( + 'single() can not be used on resultsets prefetching has_many. Use find( \%cond ) or next() instead' + ) if $attrs->{collapse}; if ($where) { if (defined $attrs->{where}) { @@ -1071,12 +1068,13 @@ sub single { } } - my @data = $self->result_source->storage->select_single( + my $data = [ $self->result_source->storage->select_single( $attrs->{from}, $attrs->{select}, $attrs->{where}, $attrs - ); - - return (@data ? ($self->_construct_object(@data))[0] : undef); + )]; + return undef unless @$data; + $self->{stashed_rows} = [ $data ]; + $self->_construct_objects->[0]; } @@ -1233,161 +1231,225 @@ first record from the resultset. sub next { my ($self) = @_; + if (my $cache = $self->get_cache) { $self->{all_cache_position} ||= 0; return $cache->[$self->{all_cache_position}++]; } + if ($self->{attrs}{cache}) { delete $self->{pager}; $self->{all_cache_position} = 1; return ($self->all)[0]; } - if ($self->{stashed_objects}) { - my $obj = shift(@{$self->{stashed_objects}}); - delete $self->{stashed_objects} unless @{$self->{stashed_objects}}; - return $obj; - } - my @row = ( - exists $self->{stashed_row} - ? @{delete $self->{stashed_row}} - : $self->cursor->next - ); - return undef unless (@row); - my ($row, @more) = $self->_construct_object(@row); - $self->{stashed_objects} = \@more if @more; - return $row; -} -sub _construct_object { - my ($self, @row) = @_; + return shift(@{$self->{stashed_objects}}) if @{ $self->{stashed_objects}||[] }; - my $info = $self->_collapse_result($self->{_attrs}{as}, \@row) - or return (); - my @new = $self->result_class->inflate_result($self->result_source, @$info); - @new = $self->{_attrs}{record_filter}->(@new) - if exists $self->{_attrs}{record_filter}; - return @new; + $self->{stashed_objects} = $self->_construct_objects + or return undef; + + return shift @{$self->{stashed_objects}}; } -sub _collapse_result { - my ($self, $as_proto, $row) = @_; +# Constructs as many objects as it can in one pass while respecting +# cursor laziness. Several modes of operation: +# +# * Always builds everything present in @{$self->{stashed_rows}} +# * If called with $fetch_all true - pulls everything off the cursor and +# builds all objects in one pass +# * If $self->_resolved_attrs->{collapse} is true, checks the order_by +# and if the resultset is ordered properly by the left side: +# * Fetches stuff off the cursor until the "master object" changes, +# and saves the last extra row (if any) in @{$self->{stashed_rows}} +# OR +# * Just fetches, and collapses/constructs everything as if $fetch_all +# was requested (there is no other way to collapse except for an +# eager cursor) +# * If no collapse is requested - just get the next row, construct and +# return +sub _construct_objects { + my ($self, $fetch_all) = @_; - my @copy = @$row; + my $rsrc = $self->result_source; + my $attrs = $self->_resolved_attrs; - # 'foo' => [ undef, 'foo' ] - # 'foo.bar' => [ 'foo', 'bar' ] - # 'foo.bar.baz' => [ 'foo.bar', 'baz' ] + 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 @construct_as = map { [ (/^(?:(.*)\.)?([^.]+)$/) ] } @$as_proto; + my $cursor = $self->cursor; - my %collapse = %{$self->{_attrs}{collapse}||{}}; + # 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 @pri_index; + if ($fetch_all) { + # FIXME SUBOPTIMAL - we can do better, cursor->next/all (well diff. methods) should return a ref + $rows = [ ($rows ? @$rows : ()), $cursor->all ]; + } + elsif( $attrs->{collapse} ) { - # if we're doing collapsing (has_many prefetch) we need to grab records - # until the PK changes, so fill @pri_index. if not, we leave it empty so - # we know we don't have to bother. + $attrs->{_ordered_for_collapse} = (!$attrs->{order_by}) ? 0 : do { + my $st = $rsrc->schema->storage; + my @ord_cols = map + { $_->[0] } + ( $st->_extract_order_criteria($attrs->{order_by}) ) + ; - # the reason for not using the collapse stuff directly is because if you - # had for e.g. two artists in a row with no cds, the collapse info for - # both would be NULL (undef) so you'd lose the second artist + my $colinfos = $st->_resolve_column_info($attrs->{from}, \@ord_cols); - # store just the index so we can check the array positions from the row - # without having to contruct the full hash + for (0 .. $#ord_cols) { + if ( + ! $colinfos->{$ord_cols[$_]} + or + $colinfos->{$ord_cols[$_]}{-result_source} != $rsrc + ) { + splice @ord_cols, $_; + last; + } + } - if (keys %collapse) { - my %pri = map { ($_ => 1) } $self->result_source->_pri_cols; - foreach my $i (0 .. $#construct_as) { - next if defined($construct_as[$i][0]); # only self table - if (delete $pri{$construct_as[$i][1]}) { - push(@pri_index, $i); + # since all we check here are the start of the order_by belonging to the + # top level $rsrc, a present identifying set will mean that the resultset + # is ordered by its leftmost table in a tsable manner + (@ord_cols and $rsrc->_identifying_column_set({ map + { $colinfos->{$_}{-colname} => $colinfos->{$_} } + @ord_cols + })) ? 1 : 0; + } unless defined $attrs->{_ordered_for_collapse}; + + 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; } - last unless keys %pri; # short circuit (Johnny Five Is Alive!) } } - # no need to do an if, it'll be empty if @pri_index is empty anyway - - my %pri_vals = map { ($_ => $copy[$_]) } @pri_index; - - my @const_rows; - - do { # no need to check anything at the front, we always want the first row + 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 ]; + } + } - my %const; + return undef unless @{$rows||[]}; - foreach my $this_as (@construct_as) { - $const{$this_as->[0]||''}{$this_as->[1]} = shift(@copy); - } + my @extra_collapser_args; + if ($attrs->{collapse} and ! $fetch_all ) { - push(@const_rows, \%const); + @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 + ); + } - } until ( # no pri_index => no collapse => drop straight out - !@pri_index - or - do { # get another row, stash it, drop out if different PK + # hotspot - skip the setter + my $res_class = $self->_result_class; - @copy = $self->cursor->next; - $self->{stashed_row} = \@copy; + 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"); + }; - # last thing in do block, counts as true if anything doesn't match + my $infmap = $attrs->{as}; - # check xor defined first for NULL vs. NOT NULL then if one is - # defined the other must be so check string equality + $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}; - grep { - (defined $pri_vals{$_} ^ defined $copy[$_]) - || (defined $pri_vals{$_} && ($pri_vals{$_} ne $copy[$_])) - } @pri_index; + 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::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 + elsif (@$rows < 60) { + for my $r (@$rows) { + $r = $inflator_cref->($res_class, $rsrc, { map { $infmap->[$_] => $r->[$_] } (0..$#$infmap) } ); + } + } + else { + eval sprintf ( + '$_ = $inflator_cref->($res_class, $rsrc, { %s }) for @$rows', + join (', ', map { "\$infmap->[$_] => \$_->[$_]" } 0..$#$infmap ) + ); + } + } + # 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}, + hri_style => 1, + prune_null_branches => 1, + }) )->($rows, @extra_collapser_args); + } + # Regular multi-object + else { - my $alias = $self->{attrs}{alias}; - my $info = []; + # 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' + ); + } - my %collapse_pos; + ( $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); - my @const_keys; + $_ = $inflator_cref->($res_class, $rsrc, @$_) for @$rows; + } - foreach my $const (@const_rows) { - scalar @const_keys or do { - @const_keys = sort { length($a) <=> length($b) } keys %$const; - }; - foreach my $key (@const_keys) { - if (length $key) { - my $target = $info; - my @parts = split(/\./, $key); - my $cur = ''; - my $data = $const->{$key}; - foreach my $p (@parts) { - $target = $target->[1]->{$p} ||= []; - $cur .= ".${p}"; - if ($cur eq ".${key}" && (my @ckey = @{$collapse{$cur}||[]})) { - # collapsing at this point and on final part - my $pos = $collapse_pos{$cur}; - CK: foreach my $ck (@ckey) { - if (!defined $pos->{$ck} || $pos->{$ck} ne $data->{$ck}) { - $collapse_pos{$cur} = $data; - delete @collapse_pos{ # clear all positioning for sub-entries - grep { m/^\Q${cur}.\E/ } keys %collapse_pos - }; - push(@$target, []); - last CK; - } - } - } - if (exists $collapse{$cur}) { - $target = $target->[-1]; - } - } - $target->[0] = $data; - } else { - $info->[0] = $const->{$key}; - } - } + # CDBI compat stuff + if ($attrs->{record_filter}) { + $_ = $attrs->{record_filter}->($_) for @$rows; } - return $info; + return $rows; } =head2 result_source @@ -1427,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); } @@ -1435,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; } @@ -1460,12 +1525,11 @@ sub count { return $self->search(@_)->count if @_ and defined $_[0]; return scalar @{ $self->get_cache } if $self->get_cache; - my $attrs = $self->_resolved_attrs_copy; + my $attrs = { %{ $self->_resolved_attrs } }; # this is a little optimization - it is faster to do the limit # adjustments in software, instead of a subquery - my $rows = delete $attrs->{rows}; - my $offset = delete $attrs->{offset}; + my ($rows, $offset) = delete @{$attrs}{qw/rows offset/}; my $crs; if ($self->_has_resolved_attr (qw/collapse group_by/)) { @@ -1536,7 +1600,6 @@ sub _count_rs { # overwrite the selector (supplied by the storage) $tmp_attrs->{select} = $rsrc->storage->_count_select ($rsrc, $attrs); $tmp_attrs->{as} = 'count'; - delete @{$tmp_attrs}{qw/columns/}; my $tmp_rs = $rsrc->resultset_class->new($rsrc, $tmp_attrs)->get_column ('count'); @@ -1558,7 +1621,7 @@ sub _count_subq_rs { # if we multi-prefetch we group_by something unique, as this is what we would # get out of the rs via ->next/->all. We *DO WANT* to clobber old group_by regardless - if ( keys %{$attrs->{collapse}} ) { + if ( $attrs->{collapse} ) { $sub_attrs->{group_by} = [ map { "$attrs->{alias}.$_" } @{ $rsrc->_identifying_column_set || $self->throw_exception( 'Unable to construct a unique group_by criteria properly collapsing the ' @@ -1596,18 +1659,22 @@ sub _count_subq_rs { my ($lquote, $rquote, $sep) = map { quotemeta $_ } ($sql_maker->_quote_chars, $sql_maker->name_sep); - my $sql = $sql_maker->_parse_rs_attrs ({ having => $attrs->{having} }); + my $having_sql = $sql_maker->_parse_rs_attrs ({ having => $attrs->{having} }); + my %seen_having; # search for both a proper quoted qualified string, for a naive unquoted scalarref # and if all fails for an utterly naive quoted scalar-with-function - while ($sql =~ / + while ($having_sql =~ / $rquote $sep $lquote (.+?) $rquote | [\s,] \w+ \. (\w+) [\s,] | [\s,] $lquote (.+?) $rquote [\s,] /gx) { - push @parts, ($1 || $2 || $3); # one of them matched if we got here + my $part = $1 || $2 || $3; # one of them matched if we got here + unless ($seen_having{$part}++) { + push @parts, $part; + } } } @@ -1678,33 +1745,22 @@ Returns all elements in the resultset. sub all { my $self = shift; if(@_) { - $self->throw_exception("all() doesn't take any arguments, you probably wanted ->search(...)->all()"); + $self->throw_exception("all() doesn't take any arguments, you probably wanted ->search(...)->all()"); } - return @{ $self->get_cache } if $self->get_cache; - - my @obj; - - if (keys %{$self->_resolved_attrs->{collapse}}) { - # Using $self->cursor->all is really just an optimisation. - # If we're collapsing has_many prefetches it probably makes - # very little difference, and this is cleaner than hacking - # _construct_object to survive the approach - $self->cursor->reset; - my @row = $self->cursor->next; - while (@row) { - push(@obj, $self->_construct_object(@row)); - @row = (exists $self->{stashed_row} - ? @{delete $self->{stashed_row}} - : $self->cursor->next); - } - } else { - @obj = map { $self->_construct_object(@$_) } $self->cursor->all; + delete @{$self}{qw/stashed_rows stashed_objects/}; + + if (my $c = $self->get_cache) { + return @$c; } - $self->set_cache(\@obj) if $self->{attrs}{cache}; + $self->cursor->reset; - return @obj; + my $objs = $self->_construct_objects('fetch_all') || []; + + $self->set_cache($objs) if $self->{attrs}{cache}; + + return @$objs; } =head2 reset @@ -1725,7 +1781,8 @@ another query. sub reset { my ($self) = @_; - delete $self->{_attrs} if exists $self->{_attrs}; + + delete @{$self}{qw/stashed_rows stashed_objects/}; $self->{all_cache_position} = 0; $self->cursor->reset; return $self; @@ -1760,152 +1817,146 @@ sub first { sub _rs_update_delete { my ($self, $op, $values) = @_; - my $cond = $self->{cond}; my $rsrc = $self->result_source; my $storage = $rsrc->schema->storage; my $attrs = { %{$self->_resolved_attrs} }; + my $join_classifications; my $existing_group_by = delete $attrs->{group_by}; - my $needs_subq = defined $existing_group_by; - # simplify the joinmap and maybe decide if a subquery is necessary - my $relation_classifications = {}; + # do we need a subquery for any reason? + my $needs_subq = ( + defined $existing_group_by + or + # if {from} is unparseable wrap a subq + ref($attrs->{from}) ne 'ARRAY' + or + # limits call for a subq + $self->_has_resolved_attr(qw/rows offset/) + ); - if (ref($attrs->{from}) eq 'ARRAY') { - # if we already know we need a subq, no point of classifying relations - if (!$needs_subq and @{$attrs->{from}} > 1) { - $attrs->{from} = $storage->_prune_unused_joins ($attrs->{from}, $attrs->{select}, $cond, $attrs); + # simplify the joinmap, so we can further decide if a subq is necessary + if (!$needs_subq and @{$attrs->{from}} > 1) { + $attrs->{from} = $storage->_prune_unused_joins ($attrs->{from}, $attrs->{select}, $self->{cond}, $attrs); - $relation_classifications = $storage->_resolve_aliastypes_from_select_args ( + # check if there are any joins left after the prune + if ( @{$attrs->{from}} > 1 ) { + $join_classifications = $storage->_resolve_aliastypes_from_select_args ( [ @{$attrs->{from}}[1 .. $#{$attrs->{from}}] ], $attrs->{select}, - $cond, + $self->{cond}, $attrs ); + + # any non-pruneable joins imply subq + $needs_subq = scalar keys %{ $join_classifications->{restricting} || {} }; } } - else { - $needs_subq ||= 1; # if {from} is unparseable assume the worst - } + # check if the head is composite (by now all joins are thrown out unless $needs_subq) + $needs_subq ||= ( + (ref $attrs->{from}[0]) ne 'HASH' + or + ref $attrs->{from}[0]{ $attrs->{from}[0]{-alias} } + ); + + my ($cond, $guard); # do we need anything like a subquery? - if ( - ! $needs_subq - and - ! keys %{ $relation_classifications->{restricting} || {} } - and - ! $self->_has_resolved_attr(qw/rows offset/) # limits call for a subq - ) { + if (! $needs_subq) { # Most databases do not allow aliasing of tables in UPDATE/DELETE. Thus # a condition containing 'me' or other table prefixes will not work # at all. Tell SQLMaker to dequalify idents via a gross hack. - my $cond = do { + $cond = do { my $sqla = $rsrc->storage->sql_maker; local $sqla->{_dequalify_idents} = 1; \[ $sqla->_recurse_where($self->{cond}) ]; }; - return $rsrc->storage->$op( - $rsrc, - $op eq 'update' ? $values : (), - $cond, - ); } - - # we got this far - means it is time to wrap a subquery - my $idcols = $rsrc->_identifying_column_set || $self->throw_exception( - sprintf( - "Unable to perform complex resultset %s() without an identifying set of columns on source '%s'", - $op, - $rsrc->source_name, - ) - ); - - # make a new $rs selecting only the PKs (that's all we really need for the subq) - delete $attrs->{$_} for qw/collapse _collapse_order_by select _prefetch_selector_range as/; - $attrs->{columns} = [ map { "$attrs->{alias}.$_" } @$idcols ]; - $attrs->{group_by} = \ ''; # FIXME - this is an evil hack, it causes the optimiser to kick in and throw away the LEFT joins - my $subrs = (ref $self)->new($rsrc, $attrs); - - if (@$idcols == 1) { - return $storage->$op ( - $rsrc, - $op eq 'update' ? $values : (), - { $idcols->[0] => { -in => $subrs->as_query } }, - ); - } - elsif ($storage->_use_multicolumn_in) { - # This is hideously ugly, but SQLA does not understand multicol IN expressions - my $sql_maker = $storage->sql_maker; - my ($sql, @bind) = @${$subrs->as_query}; - $sql = sprintf ('(%s) IN %s', # the as_query already comes with a set of parenthesis - join (', ', map { $sql_maker->_quote ($_) } @$idcols), - $sql, + else { + # we got this far - means it is time to wrap a subquery + my $idcols = $rsrc->_identifying_column_set || $self->throw_exception( + sprintf( + "Unable to perform complex resultset %s() without an identifying set of columns on source '%s'", + $op, + $rsrc->source_name, + ) ); - return $storage->$op ( - $rsrc, - $op eq 'update' ? $values : (), - \[$sql, @bind], - ); - } - else { + # make a new $rs selecting only the PKs (that's all we really need for the subq) + delete $attrs->{$_} for qw/collapse select _prefetch_selector_range as/; + $attrs->{columns} = [ map { "$attrs->{alias}.$_" } @$idcols ]; + $attrs->{group_by} = \ ''; # FIXME - this is an evil hack, it causes the optimiser to kick in and throw away the LEFT joins + my $subrs = (ref $self)->new($rsrc, $attrs); - # if all else fails - get all primary keys and operate over a ORed set - # wrap in a transaction for consistency - # this is where the group_by starts to matter - if ( - $existing_group_by - or - keys %{ $relation_classifications->{multiplying} || {} } - ) { - # make sure if there is a supplied group_by it matches the columns compiled above - # perfectly. Anything else can not be sanely executed on most databases so croak - # right then and there - if ($existing_group_by) { - my @current_group_by = map - { $_ =~ /\./ ? $_ : "$attrs->{alias}.$_" } - @$existing_group_by - ; - - if ( - join ("\x00", sort @current_group_by) - ne - join ("\x00", sort @{$attrs->{columns}} ) - ) { - $self->throw_exception ( - "You have just attempted a $op operation on a resultset which does group_by" - . ' on columns other than the primary keys, while DBIC internally needs to retrieve' - . ' the primary keys in a subselect. All sane RDBMS engines do not support this' - . ' kind of queries. Please retry the operation with a modified group_by or' - . ' without using one at all.' - ); + if (@$idcols == 1) { + $cond = { $idcols->[0] => { -in => $subrs->as_query } }; + } + elsif ($storage->_use_multicolumn_in) { + # no syntax for calling this properly yet + # !!! EXPERIMENTAL API !!! WILL CHANGE !!! + $cond = $storage->sql_maker->_where_op_multicolumn_in ( + $idcols, # how do I convey a list of idents...? can binds reside on lhs? + $subrs->as_query + ), + } + else { + # if all else fails - get all primary keys and operate over a ORed set + # wrap in a transaction for consistency + # this is where the group_by/multiplication starts to matter + if ( + $existing_group_by + or + keys %{ $join_classifications->{multiplying} || {} } + ) { + # make sure if there is a supplied group_by it matches the columns compiled above + # perfectly. Anything else can not be sanely executed on most databases so croak + # right then and there + if ($existing_group_by) { + my @current_group_by = map + { $_ =~ /\./ ? $_ : "$attrs->{alias}.$_" } + @$existing_group_by + ; + + if ( + join ("\x00", sort @current_group_by) + ne + join ("\x00", sort @{$attrs->{columns}} ) + ) { + $self->throw_exception ( + "You have just attempted a $op operation on a resultset which does group_by" + . ' on columns other than the primary keys, while DBIC internally needs to retrieve' + . ' the primary keys in a subselect. All sane RDBMS engines do not support this' + . ' kind of queries. Please retry the operation with a modified group_by or' + . ' without using one at all.' + ); + } } - } - $subrs = $subrs->search({}, { group_by => $attrs->{columns} }); - } + $subrs = $subrs->search({}, { group_by => $attrs->{columns} }); + } - my $guard = $storage->txn_scope_guard; + $guard = $storage->txn_scope_guard; - my @op_condition; - for my $row ($subrs->cursor->all) { - push @op_condition, { map - { $idcols->[$_] => $row->[$_] } - (0 .. $#$idcols) - }; + $cond = []; + for my $row ($subrs->cursor->all) { + push @$cond, { map + { $idcols->[$_] => $row->[$_] } + (0 .. $#$idcols) + }; + } } + } - my $res = $storage->$op ( - $rsrc, - $op eq 'update' ? $values : (), - \@op_condition, - ); + my $res = $storage->$op ( + $rsrc, + $op eq 'update' ? $values : (), + $cond, + ); - $guard->commit; + $guard->commit if $guard; - return $res; - } + return $res; } =head2 update @@ -2038,28 +2089,55 @@ sub delete_all { =over 4 -=item Arguments: \@data; +=item Arguments: [ \@column_list, \@row_values+ ] | [ \%col_data+ ] + +=item Return Value: L<\@result_objects|DBIx::Class::Manual::ResultClass> (scalar context) | L<@result_objects|DBIx::Class::Manual::ResultClass> (list context) =back -Accepts either an arrayref of hashrefs or alternatively an arrayref of arrayrefs. -For the arrayref of hashrefs style each hashref should be a structure suitable -for submitting to a $resultset->create(...) method. +Accepts either an arrayref of hashrefs or alternatively an arrayref of +arrayrefs. -In void context, C in L is used -to insert the data, as this is a faster method. +=over + +=item NOTE -Otherwise, each set of data is inserted into the database using -L, and the resulting objects are -accumulated into an array. The array itself, or an array reference -is returned depending on scalar or list context. +The context of this method call has an important effect on what is +submitted to storage. In void context data is fed directly to fastpath +insertion routines provided by the underlying storage (most often +L), bypassing the L and +L calls on the +L class, including any +augmentation of these methods provided by components. For example if you +are using something like L to create primary +keys for you, you will find that your PKs are empty. In this case you +will have to explicitly force scalar or list context in order to create +those values. + +=back + +In non-void (scalar or list) context, this method is simply a wrapper +for L. Depending on list or scalar context either a list of +L objects or an arrayref +containing these objects is returned. + +When supplying data in "arrayref of arrayrefs" invocation style, the +first element should be a list of column names and each subsequent +element should be a data value in the earlier specified column order. +For example: -Example: Assuming an Artist Class that has many CDs Classes relating: + $Arstist_rs->populate([ + [ qw( artistid name ) ], + [ 100, 'A Formally Unknown Singer' ], + [ 101, 'A singer that jumped the shark two albums ago' ], + [ 102, 'An actually cool singer' ], + ]); - my $Artist_rs = $schema->resultset("Artist"); +For the arrayref of hashrefs style each hashref should be a structure +suitable for passing to L. Multi-create is also permitted with +this syntax. - ## Void Context Example - $Artist_rs->populate([ + $schema->resultset("Artist")->populate([ { artistid => 4, name => 'Manufactured Crap', cds => [ { title => 'My First CD', year => 2006 }, { title => 'Yet More Tweeny-Pop crap', year => 2007 }, @@ -2073,37 +2151,11 @@ Example: Assuming an Artist Class that has many CDs Classes relating: }, ]); - ## Array Context Example - my ($ArtistOne, $ArtistTwo, $ArtistThree) = $Artist_rs->populate([ - { name => "Artist One"}, - { name => "Artist Two"}, - { name => "Artist Three", cds=> [ - { title => "First CD", year => 2007}, - { title => "Second CD", year => 2008}, - ]} - ]); - - print $ArtistOne->name; ## response is 'Artist One' - print $ArtistThree->cds->count ## reponse is '2' - -For the arrayref of arrayrefs style, the first element should be a list of the -fieldsnames to which the remaining elements are rows being inserted. For -example: - - $Arstist_rs->populate([ - [qw/artistid name/], - [100, 'A Formally Unknown Singer'], - [101, 'A singer that jumped the shark two albums ago'], - [102, 'An actually cool singer'], - ]); - -Please note an important effect on your data when choosing between void and -wantarray context. Since void context goes straight to C in -L this will skip any component that is overriding -C. So if you are using something like L to -create primary keys for you, you will find that your PKs are empty. In this -case you will have to use the wantarray context in order to create those -values. +If you attempt a void-context multi-create as in the example above (each +Artist also has the related list of CDs), and B supply the +necessary autoinc foreign key information, this method will proxy to the +less efficient L, and then throw the Result objects away. In this +case there are obviously no benefits to using this method over L. =cut @@ -2116,10 +2168,7 @@ sub populate { return unless @$data; if(defined wantarray) { - my @created; - foreach my $item (@$data) { - push(@created, $self->create($item)); - } + my @created = map { $self->create($_) } @$data; return wantarray ? @created : \@created; } else { @@ -2174,14 +2223,12 @@ sub populate { ## inherit the data locked in the conditions of the resultset my ($rs_data) = $self->_merge_with_rscond({}); delete @{$rs_data}{@columns}; - my @inherit_cols = keys %$rs_data; - my @inherit_data = values %$rs_data; ## do bulk insert on current row $rsrc->storage->insert_bulk( $rsrc, - [@columns, @inherit_cols], - [ map { [ @$_{@columns}, @inherit_data ] } @$data ], + [@columns, keys %$rs_data], + [ map { [ @$_{@columns}, values %$rs_data ] } @$data ], ); ## do the has_many relationships @@ -2273,7 +2320,7 @@ sub pager { # throw away the paging flags and re-run the count (possibly # with a subselect) to get the real total count my $count_attrs = { %$attrs }; - delete $count_attrs->{$_} for qw/rows offset page pager/; + delete @{$count_attrs}{qw/rows offset page pager/}; my $total_rs = (ref $self)->new($self->result_source, $count_attrs); @@ -2530,7 +2577,7 @@ This is generally used as the RHS for a subquery. sub as_query { my $self = shift; - my $attrs = $self->_resolved_attrs_copy; + my $attrs = { %{ $self->_resolved_attrs } }; # For future use: # @@ -3013,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); @@ -3040,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); @@ -3069,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; }; } @@ -3112,9 +3158,7 @@ source alias of the current result set: =cut sub current_source_alias { - my ($self) = @_; - - return ($self->{attrs} || {})->{alias} || 'me'; + return (shift->{attrs} || {})->{alias} || 'me'; } =head2 as_subselect_rs @@ -3296,11 +3340,39 @@ sub _chain_relationship { return {%$attrs, from => $from, seen_join => $seen}; } -# too many times we have to do $attrs = { %{$self->_resolved_attrs} } -sub _resolved_attrs_copy { - my $self = shift; - return { %{$self->_resolved_attrs (@_)} }; -} +# 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; @@ -3347,14 +3419,10 @@ sub _resolved_attrs { if $attrs->{select}; # assume all unqualified selectors to apply to the current alias (legacy stuff) - for (@sel) { - $_ = (ref $_ or $_ =~ /\./) ? $_ : "$alias.$_"; - } + $_ = (ref $_ or $_ =~ /\./) ? $_ : "$alias.$_" for @sel; - # disqualify all $alias.col as-bits (collapser mandated) - for (@as) { - $_ = ($_ =~ /^\Q$alias.\E(.+)$/) ? $1 : $_; - } + # disqualify all $alias.col as-bits (inflate-map mandated) + $_ = ($_ =~ /^\Q$alias.\E(.+)$/) ? $1 : $_ for @as; # de-duplicate the result (remove *identical* select/as pairs) # and also die on duplicate {as} pointing to different {select}s @@ -3377,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; @@ -3441,15 +3517,17 @@ sub _resolved_attrs { } } - $attrs->{collapse} ||= {}; - if ($attrs->{prefetch}) { + # generate selections based on the prefetch helper + my $prefetch; + $prefetch = $self->_merge_joinpref_attr( {}, delete $attrs->{prefetch} ) + if defined $attrs->{prefetch}; + + if ($prefetch) { $self->throw_exception("Unable to prefetch, resultset contains an unnamed selector $attrs->{_dark_selector}{string}") if $attrs->{_dark_selector}; - my $prefetch = $self->_merge_joinpref_attr( {}, delete $attrs->{prefetch} ); - - my $prefetch_ordering = []; + $attrs->{collapse} = 1; # this is a separate structure (we don't look in {from} directly) # as the resolver needs to shift things off the lists to work @@ -3472,8 +3550,7 @@ sub _resolved_attrs { } } - my @prefetch = - $source->_resolve_prefetch( $prefetch, $alias, $join_map, $prefetch_ordering, $attrs->{collapse} ); + my @prefetch = $source->_resolve_prefetch( $prefetch, $alias, $join_map ); # we need to somehow mark which columns came from prefetch if (@prefetch) { @@ -3483,9 +3560,60 @@ sub _resolved_attrs { push @{ $attrs->{select} }, (map { $_->[0] } @prefetch); push @{ $attrs->{as} }, (map { $_->[1] } @prefetch); + } + + if ( ! List::Util::first { $_ =~ /\./ } @{$attrs->{as}} ) { + $attrs->{_single_resultclass_inflation} = 1; + $attrs->{collapse} = 0; + } + + # run through the resulting joinstructure (starting from our current slot) + # and unset collapse if proven unnesessary + # + # also while we are at it find out if the current root source has + # been premultiplied by previous related_source chaining + # + # this allows to predict whether a root object with all other relation + # data set to NULL is in fact unique + if ($attrs->{collapse}) { + + if (ref $attrs->{from} eq 'ARRAY') { + + if (@{$attrs->{from}} <= 1) { + # no joins - no collapse + $attrs->{collapse} = 0; + } + else { + # find where our table-spec starts + my @fromlist = @{$attrs->{from}}; + while (@fromlist) { + my $t = shift @fromlist; + + my $is_multi; + # me vs join from-spec distinction - a ref means non-root + if (ref $t eq 'ARRAY') { + $t = $t->[0]; + $is_multi ||= ! $t->{-is_single}; + } + last if ($t->{-alias} && $t->{-alias} eq $alias); + $attrs->{_main_source_premultiplied} ||= $is_multi; + } - push( @{$attrs->{order_by}}, @$prefetch_ordering ); - $attrs->{_collapse_order_by} = \@$prefetch_ordering; + # no non-singles remaining, nor any premultiplication - nothing to collapse + if ( + ! $attrs->{_main_source_premultiplied} + and + ! List::Util::first { ! $_->[0]{-is_single} } @fromlist + ) { + $attrs->{collapse} = 0; + } + } + } + + else { + # if we can not analyze the from - err on the side of safety + $attrs->{_main_source_premultiplied} = 1; + } } # if both page and offset are specified, produce a combined offset @@ -3612,7 +3740,7 @@ sub _merge_joinpref_attr { $seen_keys->{$import_key} = 1; # don't merge the same key twice } - return $orig; + return @$orig ? $orig : (); } { @@ -3708,7 +3836,8 @@ sub STORABLE_freeze { my $to_serialize = { %$self }; # A cursor in progress can't be serialized (and would make little sense anyway) - delete $to_serialize->{cursor}; + # the parser can be regenerated (and can't be serialized) + 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') { @@ -3745,6 +3874,10 @@ sub throw_exception { } } +1; + +__END__ + # XXX: FIXME: Attributes docs need clearing up =head1 ATTRIBUTES @@ -3794,7 +3927,7 @@ syntax as outlined above. =over 4 -=item Value: \@columns +=item Value: \@columns | \%columns | $column =back @@ -3896,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 @@ -3946,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 @@ -4009,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 @@ -4020,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' - } - } - ); +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: -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. - -=head3 Using L with L - -L implies a L with the equivalent argument, and is -properly merged with any existing L specification. So the -following: + my $rs = $schema->resultset('CD')->search({}, { + '+columns' => [ qw/ tracks.title tracks.position / ], + join => 'tracks', + collapse => 1, + }); - my $rs = $schema->resultset('CD')->search( - {'record_label.name' => 'Music Product Ltd.'}, - { - join => {artist => 'record_label'}, - prefetch => 'artist', - } - ); +While executing the following query: -... will work, searching on the record label's name, but only -prefetching the C. + SELECT me.*, tracks.title, tracks.position + FROM cd me + LEFT JOIN track tracks + ON tracks.cdid = me.cdid -=head3 Using L with L / L / L / L +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, with each +CD object containing exactly one of all fetched Track data. -L implies a L/L with the fields of the -prefetched relations. So given: +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. - my $rs = $schema->resultset('CD')->search( - undef, - { - select => ['cd.title'], - as => ['cd_title'], - prefetch => 'artist', - } - ); +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. -The L becomes: C<'cd.title', 'artist.*'> and the L -becomes: C<'cd_title', 'artist.*'>. +Setting this attribute on a resultset that does not join any has_many +relations is a no-op. -=head3 CAVEATS +For a more in depth discussion see L. -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 @@ -4376,6 +4457,130 @@ 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 is 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. + +L 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 @@ -4432,6 +4637,3 @@ See L and L in You may distribute this code under the same terms as Perl itself. -=cut - -1;