use base qw/DBIx::Class/;
use DBIx::Class::Carp;
use DBIx::Class::ResultSetColumn;
-use Scalar::Util qw/blessed weaken/;
+use Scalar::Util qw/blessed weaken reftype/;
+use DBIx::Class::_Util qw(
+ fail_on_internal_wantarray is_plain_value is_literal_value
+);
use Try::Tiny;
use Data::Compare (); # no imports!!! guard against insane architecture
'bool' => "_bool",
fallback => 1;
+# this is real - CDBICompat overrides it with insanity
+# yes, prototype won't matter, but that's for now ;)
+sub _bool () { 1 }
+
__PACKAGE__->mk_group_accessors('simple' => qw/_result_class result_source/);
=head1 NAME
=head3 Resolving conditions and attributes
-When a resultset is chained from another resultset i.e.
-C<my $new_rs = $old_rs->search(\%extra_cond, \%attrs)>, conditions
+When a resultset is chained from another resultset (e.g.:
+C<< my $new_rs = $old_rs->search(\%extra_cond, \%attrs) >>), conditions
and attributes with the same keys need resolving.
-If any of L</columns>, L</select>, L</as> are present they reset the
+If any of L</columns>, L</select>, L</as> are present, they reset the
original selection, and start the selection "clean".
-L</join>, L</prefetch>, L</+columns>, L</+select>, L</+as> attributes
+The L</join>, L</prefetch>, L</+columns>, L</+select>, L</+as> attributes
are merged into the existing ones from the original resultset.
The L</where> and L</having> attributes, and any search conditions, are
my ($source, $attrs) = @_;
$source = $source->resolve
if $source->isa('DBIx::Class::ResultSourceHandle');
+
$attrs = { %{$attrs||{}} };
+ delete @{$attrs}{qw(_last_sqlmaker_alias_map _related_results_construction)};
if ($attrs->{page}) {
$attrs->{rows} ||= 10;
For a list of attributes that can be passed to C<search>, see
L</ATTRIBUTES>. For more examples of using this function, see
-L<Searching|DBIx::Class::Manual::Cookbook/Searching>. For a complete
-documentation for the first argument, see L<SQL::Abstract>
+L<Searching|DBIx::Class::Manual::Cookbook/SEARCHING>. For a complete
+documentation for the first argument, see L<SQL::Abstract/"WHERE CLAUSES">
and its extension L<DBIx::Class::SQLMaker>.
For more help on using joins with search, see L<DBIx::Class::Manual::Joining>.
my $rs = $self->search_rs( @_ );
if (wantarray) {
+ DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_WANTARRAY and my $sog = fail_on_internal_wantarray($rs);
return $rs->all;
}
elsif (defined wantarray) {
my $cache;
my %safe = (alias => 1, cache => 1);
if ( ! List::Util::first { !$safe{$_} } keys %$call_attrs and (
- ! defined $_[0]
+ ! defined $call_cond
or
- ref $_[0] eq 'HASH' && ! keys %{$_[0]}
+ ref $call_cond eq 'HASH' && ! keys %$call_cond
or
- ref $_[0] eq 'ARRAY' && ! @{$_[0]}
+ ref $call_cond eq 'ARRAY' && ! @$call_cond
)) {
$cache = $self->get_cache;
}
my $old_attrs = { %{$self->{attrs}} };
- my $old_having = delete $old_attrs->{having};
- my $old_where = delete $old_attrs->{where};
+ my ($old_having, $old_where) = delete @{$old_attrs}{qw(having where)};
my $new_attrs = { %$old_attrs };
# older deprecated name, use only if {columns} is not there
if (my $c = delete $new_attrs->{cols}) {
+ carp_unique( "Resultset attribute 'cols' is deprecated, use 'columns' instead" );
if ($new_attrs->{columns}) {
carp "Resultset specifies both the 'columns' and the legacy 'cols' attributes - ignoring 'cols'";
}
my ($self, $attrs) = @_;
# legacy syntax
- $attrs->{'+columns'} = $self->_merge_attr($attrs->{'+columns'}, delete $attrs->{include_columns})
- if exists $attrs->{include_columns};
+ if ( exists $attrs->{include_columns} ) {
+ carp_unique( "Resultset attribute 'include_columns' is deprecated, use '+columns' instead" );
+ $attrs->{'+columns'} = $self->_merge_attr(
+ $attrs->{'+columns'}, delete $attrs->{include_columns}
+ );
+ }
# columns are always placed first, however
sub _stack_cond {
my ($self, $left, $right) = @_;
- # collapse single element top-level conditions
- # (single pass only, unlikely to need recursion)
- for ($left, $right) {
- if (ref $_ eq 'ARRAY') {
- if (@$_ == 0) {
- $_ = undef;
- }
- elsif (@$_ == 1) {
- $_ = $_->[0];
- }
- }
- elsif (ref $_ eq 'HASH') {
- my ($first, $more) = keys %$_;
+ (
+ (ref $_ eq 'ARRAY' and !@$_)
+ or
+ (ref $_ eq 'HASH' and ! keys %$_)
+ ) and $_ = undef for ($left, $right);
- # empty hash
- if (! defined $first) {
- $_ = undef;
- }
- # one element hash
- elsif (! defined $more) {
- if ($first eq '-and' and ref $_->{'-and'} eq 'HASH') {
- $_ = $_->{'-and'};
- }
- elsif ($first eq '-or' and ref $_->{'-or'} eq 'ARRAY') {
- $_ = $_->{'-or'};
- }
- }
- }
+ # either on of the two undef or both undef
+ if ( ( (defined $left) xor (defined $right) ) or ! defined $left ) {
+ return defined $left ? $left : $right;
}
- # merge hashes with weeding out of duplicates (simple cases only)
- if (ref $left eq 'HASH' and ref $right eq 'HASH') {
+ my $cond = $self->result_source->schema->storage->_collapse_cond({ -and => [$left, $right] });
- # shallow copy to destroy
- $right = { %$right };
- for (grep { exists $right->{$_} } keys %$left) {
- # the use of eq_deeply here is justified - the rhs of an
- # expression can contain a lot of twisted weird stuff
- delete $right->{$_} if Data::Compare::Compare( $left->{$_}, $right->{$_} );
- }
+ for my $c (grep { ref $cond->{$_} eq 'ARRAY' and ($cond->{$_}[0]||'') eq '-and' } keys %$cond) {
- $right = undef unless keys %$right;
- }
+ my @vals = sort @{$cond->{$c}}[ 1..$#{$cond->{$c}} ];
+ my @fin = shift @vals;
+ for my $v (@vals) {
+ push @fin, $v unless Data::Compare::Compare( $fin[-1], $v );
+ }
- if (defined $left xor defined $right) {
- return defined $left ? $left : $right;
- }
- elsif (! defined $left) {
- return undef;
- }
- else {
- return { -and => [ $left, $right ] };
+ $cond->{$c} = (@fin == 1) ? $fin[0] : [-and => @fin ];
}
+
+ $cond;
}
=head2 search_literal
method. It is equivalent to calling C<< $schema->search(\[]) >>, but if you
want to ensure columns are bound correctly, use L</search>.
-See L<DBIx::Class::Manual::Cookbook/Searching> and
+See L<DBIx::Class::Manual::Cookbook/SEARCHING> and
L<DBIx::Class::Manual::FAQ/Searching> for searching techniques that do not
require C<search_literal>.
next if $keyref eq 'ARRAY'; # has_many for multi_create
- my $rel_q = $rsrc->_resolve_condition(
+ my ($rel_cond, $crosstable) = $rsrc->_resolve_condition(
$relinfo->{cond}, $val, $key, $key
);
- die "Can't handle complex relationship conditions in find" if ref($rel_q) ne 'HASH';
- @related{keys %$rel_q} = values %$rel_q;
+
+ $self->throw_exception("Complex condition via relationship '$key' is unsupported in find()")
+ if $crosstable or ref($rel_cond) ne 'HASH';
+
+ # supplement
+ @related{keys %$rel_cond} = values %$rel_cond;
}
}
my $self = shift;
return $self->{cursor} ||= do {
- my $attrs = { %{$self->_resolved_attrs } };
+ my $attrs = $self->_resolved_attrs;
$self->result_source->storage->select(
$attrs->{from}, $attrs->{select}, $attrs->{where}, $attrs
);
my $attrs = { %{$self->_resolved_attrs} };
$self->throw_exception(
- 'single() can not be used on resultsets prefetching has_many. Use find( \%cond ) or next() instead'
+ 'single() can not be used on resultsets collapsing a has_many. Use find( \%cond ) or next() instead'
) if $attrs->{collapse};
if ($where) {
$attrs->{from}, $attrs->{select},
$attrs->{where}, $attrs
)];
- return undef unless @$data;
- $self->{stashed_rows} = [ $data ];
- $self->_construct_objects->[0];
-}
-
-# _collapse_query
-#
-# Recursively collapse the query, accumulating values for each column.
-
-sub _collapse_query {
- my ($self, $query, $collapsed) = @_;
-
- $collapsed ||= {};
-
- if (ref $query eq 'ARRAY') {
- foreach my $subquery (@$query) {
- next unless ref $subquery; # -or
- $collapsed = $self->_collapse_query($subquery, $collapsed);
- }
- }
- elsif (ref $query eq 'HASH') {
- if (keys %$query and (keys %$query)[0] eq '-and') {
- foreach my $subquery (@{$query->{-and}}) {
- $collapsed = $self->_collapse_query($subquery, $collapsed);
- }
- }
- else {
- foreach my $col (keys %$query) {
- my $value = $query->{$col};
- $collapsed->{$col}{$value}++;
- }
- }
- }
-
- return $collapsed;
+ return undef unless @$data;
+ $self->{_stashed_rows} = [ $data ];
+ $self->_construct_results->[0];
}
=head2 get_column
$attrs->{offset} += $min;
$attrs->{rows} = ($max ? ($max - $min + 1) : 1);
return $self->search(undef, $attrs);
- #my $slice = (ref $self)->new($self->result_source, $attrs);
- #return (wantarray ? $slice->all : $slice);
}
=head2 next
return ($self->all)[0];
}
- return shift(@{$self->{stashed_objects}}) if @{ $self->{stashed_objects}||[] };
+ return shift(@{$self->{_stashed_results}}) if @{ $self->{_stashed_results}||[] };
- $self->{stashed_objects} = $self->_construct_objects
+ $self->{_stashed_results} = $self->_construct_results
or return undef;
- return shift @{$self->{stashed_objects}};
+ return shift @{$self->{_stashed_results}};
}
-# Constructs as many objects as it can in one pass while respecting
+# Constructs as many results as it can in one pass while respecting
# cursor laziness. Several modes of operation:
#
-# * Always builds everything present in @{$self->{stashed_rows}}
+# * 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
+# builds all result structures (or 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}}
+# 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 {
+sub _construct_results {
my ($self, $fetch_all) = @_;
my $rsrc = $self->result_source;
my $attrs = $self->_resolved_attrs;
- if (!$fetch_all and ! $attrs->{order_by} and $attrs->{collapse}) {
+ if (
+ ! $fetch_all
+ and
+ ! $attrs->{order_by}
+ and
+ $attrs->{collapse}
+ and
+ my @pcols = $rsrc->primary_columns
+ ) {
# default order for collapsing unless the user asked for something
- $attrs->{order_by} = [ map { join '.', $attrs->{alias}, $_} $rsrc->primary_columns ];
+ $attrs->{order_by} = [ map { join '.', $attrs->{alias}, $_} @pcols ];
$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};
+ # _construct_results. Not regrowing the array twice matters a lot...
+ # a surprising amount actually
+ my $rows = delete $self->{_stashed_rows};
+
+ my $cursor; # we may not need one at all
+
+ my $did_fetch_all = $fetch_all;
if ($fetch_all) {
# FIXME SUBOPTIMAL - we can do better, cursor->next/all (well diff. methods) should return a ref
- $rows = [ ($rows ? @$rows : ()), $cursor->all ];
+ $rows = [ ($rows ? @$rows : ()), $self->cursor->all ];
}
elsif( $attrs->{collapse} ) {
- $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}) )
- ;
-
- my $colinfos = $st->_resolve_column_info($attrs->{from}, \@ord_cols);
-
- for (0 .. $#ord_cols) {
- if (
- ! $colinfos->{$ord_cols[$_]}
- or
- $colinfos->{$ord_cols[$_]}{-result_source} != $rsrc
- ) {
- splice @ord_cols, $_;
- last;
- }
- }
+ # a cursor will need to be closed over in case of collapse
+ $cursor = $self->cursor;
- # 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};
+ $attrs->{_ordered_for_collapse} = (
+ (
+ $attrs->{order_by}
+ and
+ $rsrc->schema
+ ->storage
+ ->_main_source_order_by_portion_is_stable($rsrc, $attrs->{order_by}, $attrs->{where})
+ ) ? 1 : 0
+ ) unless defined $attrs->{_ordered_for_collapse};
if (! $attrs->{_ordered_for_collapse}) {
- $fetch_all = 1;
+ $did_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
+ # FIXME ENCAPSULATION - encapsulation breach, cursor method additions pending
if (! $cursor->{_done}) {
$rows = [ ($rows ? @$rows : ()), $cursor->all ];
$cursor->{_done} = 1;
}
}
- if (! $fetch_all and ! @{$rows||[]} ) {
+ if (! $did_fetch_all and ! @{$rows||[]} ) {
# FIXME SUBOPTIMAL - we can do better, cursor->next/all (well diff. methods) should return a ref
+ $cursor ||= $self->cursor;
if (scalar (my @r = $cursor->next) ) {
$rows = [ \@r ];
}
return undef unless @{$rows||[]};
- my @extra_collapser_args;
- if ($attrs->{collapse} and ! $fetch_all ) {
+ # sanity check - people are too clever for their own good
+ if ($attrs->{collapse} and my $aliastypes = $attrs->{_last_sqlmaker_alias_map} ) {
- @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 $multiplied_selectors;
+ for my $sel_alias ( grep { $_ ne $attrs->{alias} } keys %{ $aliastypes->{selecting} } ) {
+ if (
+ $aliastypes->{multiplying}{$sel_alias}
+ or
+ $aliastypes->{premultiplied}{$sel_alias}
+ ) {
+ $multiplied_selectors->{$_} = 1 for values %{$aliastypes->{selecting}{$sel_alias}{-seen_columns}}
+ }
+ }
+
+ for my $i (0 .. $#{$attrs->{as}} ) {
+ my $sel = $attrs->{select}[$i];
+
+ if (ref $sel eq 'SCALAR') {
+ $sel = $$sel;
+ }
+ elsif( ref $sel eq 'REF' and ref $$sel eq 'ARRAY' ) {
+ $sel = $$sel->[0];
+ }
+
+ $self->throw_exception(
+ 'Result collapse not possible - selection from a has_many source redirected to the main object'
+ ) if ($multiplied_selectors->{$sel} and $attrs->{as}[$i] !~ /\./);
+ }
}
# hotspot - skip the setter
my $infmap = $attrs->{as};
- $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};
+ $self->{_result_inflator}{is_core_row} = ( (
+ $inflator_cref
+ ==
+ ( \&DBIx::Class::Row::inflate_result || die "No ::Row::inflate_result() - can't happen" )
+ ) ? 1 : 0 ) unless defined $self->{_result_inflator}{is_core_row};
- if ($attrs->{_single_resultclass_inflation}) {
+ $self->{_result_inflator}{is_hri} = ( (
+ ! $self->{_result_inflator}{is_core_row}
+ and
+ $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->{_related_results_construction}) {
# construct a much simpler array->hash folder for the one-table cases right here
if ($self->{_result_inflator}{is_hri}) {
for my $r (@$rows) {
);
}
}
- # 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 $parser_type =
+ $self->{_result_inflator}{is_hri} ? 'hri'
+ : $self->{_result_inflator}{is_core_row} ? 'classic_pruning'
+ : 'classic_nonpruning'
+ ;
- # 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({
+ # $args and $attrs to _mk_row_parser are separated to delineate what is
+ # core collapser stuff and what is dbic $rs specific
+ @{$self->{_row_parser}{$parser_type}}{qw(cref nullcheck)} = $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);
+ hri_style => $self->{_result_inflator}{is_hri},
+ prune_null_branches => $self->{_result_inflator}{is_hri} || $self->{_result_inflator}{is_core_row},
+ }, $attrs) unless $self->{_row_parser}{$parser_type}{cref};
+
+ # column_info metadata historically hasn't been too reliable.
+ # We need to start fixing this somehow (the collapse resolver
+ # can't work without it). Add an explicit check for the *main*
+ # result, hopefully this will gradually weed out such errors
+ #
+ # FIXME - this is a temporary kludge that reduces performance
+ # It is however necessary for the time being
+ my ($unrolled_non_null_cols_to_check, $err);
+
+ if (my $check_non_null_cols = $self->{_row_parser}{$parser_type}{nullcheck} ) {
+
+ $err =
+ 'Collapse aborted due to invalid ResultSource metadata - the following '
+ . 'selections are declared non-nullable but NULLs were retrieved: '
+ ;
+
+ my @violating_idx;
+ COL: for my $i (@$check_non_null_cols) {
+ ! defined $_->[$i] and push @violating_idx, $i and next COL for @$rows;
+ }
+
+ $self->throw_exception( $err . join (', ', map { "'$infmap->[$_]'" } @violating_idx ) )
+ if @violating_idx;
+
+ $unrolled_non_null_cols_to_check = join (',', @$check_non_null_cols);
+ }
- $_ = $inflator_cref->($res_class, $rsrc, @$_) for @$rows;
+ my $next_cref =
+ ($did_fetch_all or ! $attrs->{collapse}) ? undef
+ : defined $unrolled_non_null_cols_to_check ? eval sprintf <<'EOS', $unrolled_non_null_cols_to_check
+sub {
+ # FIXME SUBOPTIMAL - we can do better, cursor->next/all (well diff. methods) should return a ref
+ my @r = $cursor->next or return;
+ if (my @violating_idx = grep { ! defined $r[$_] } (%s) ) {
+ $self->throw_exception( $err . join (', ', map { "'$infmap->[$_]'" } @violating_idx ) )
}
+ \@r
+}
+EOS
+ : sub {
+ # FIXME SUBOPTIMAL - we can do better, cursor->next/all (well diff. methods) should return a ref
+ my @r = $cursor->next or return;
+ \@r
+ }
+ ;
- # CDBI compat stuff
- if ($attrs->{record_filter}) {
- $_ = $attrs->{record_filter}->($_) for @$rows;
+ $self->{_row_parser}{$parser_type}{cref}->(
+ $rows,
+ $next_cref ? ( $next_cref, $self->{_stashed_rows} = [] ) : (),
+ );
+
+ # Special-case multi-object HRI - there is no $inflator_cref pass
+ unless ($self->{_result_inflator}{is_hri}) {
+ $_ = $inflator_cref->($res_class, $rsrc, @$_) for @$rows
+ }
}
+ # The @$rows check seems odd at first - why wouldn't we want to warn
+ # regardless? The issue is things like find() etc, where the user
+ # *knows* only one result will come back. In these cases the ->all
+ # is not a pessimization, but rather something we actually want
+ carp_unique(
+ 'Unable to properly collapse has_many results in iterator mode due '
+ . 'to order criteria - performed an eager cursor slurp underneath. '
+ . 'Consider using ->all() instead'
+ ) if ( ! $fetch_all and @$rows > 1 );
+
return $rows;
}
my ($self, $result_class) = @_;
if ($result_class) {
- unless (ref $result_class) { # don't fire this for an object
- $self->ensure_class_loaded($result_class);
+ # don't fire this for an object
+ $self->ensure_class_loaded($result_class)
+ unless ref($result_class);
+
+ if ($self->get_cache) {
+ carp_unique('Changing the result_class of a ResultSet instance with cached results is a noop - the cache contents will not be altered');
+ }
+ # FIXME ENCAPSULATION - encapsulation breach, cursor method additions pending
+ elsif ($self->{cursor} && $self->{cursor}{_pos}) {
+ $self->throw_exception('Changing the result_class of a ResultSet instance with an active cursor is not supported');
}
+
$self->_result_class($result_class);
- # THIS LINE WOULD BE A BUG - this accessor specifically exists to
- # 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};
}
# software based limiting can not be ported if this $rs is to be used
# in a subquery itself (i.e. ->as_query)
if ($self->_has_resolved_attr (qw/collapse group_by offset rows/)) {
- return $self->_count_subq_rs;
+ return $self->_count_subq_rs($self->{_attrs});
}
else {
- return $self->_count_rs;
+ return $self->_count_rs($self->{_attrs});
}
}
my ($self, $attrs) = @_;
my $rsrc = $self->result_source;
- $attrs ||= $self->_resolved_attrs;
my $tmp_attrs = { %$attrs };
# take off any limits, record_filter is cdbi, and no point of ordering nor locking a count
delete @{$tmp_attrs}{qw/rows offset order_by record_filter for/};
# overwrite the selector (supplied by the storage)
- $tmp_attrs->{select} = $rsrc->storage->_count_select ($rsrc, $attrs);
- $tmp_attrs->{as} = 'count';
-
- my $tmp_rs = $rsrc->resultset_class->new($rsrc, $tmp_attrs)->get_column ('count');
-
- return $tmp_rs;
+ $rsrc->resultset_class->new($rsrc, {
+ %$tmp_attrs,
+ select => $rsrc->storage->_count_select ($rsrc, $attrs),
+ as => 'count',
+ })->get_column ('count');
}
#
my ($self, $attrs) = @_;
my $rsrc = $self->result_source;
- $attrs ||= $self->_resolved_attrs;
my $sub_attrs = { %$attrs };
# extra selectors do not go in the subquery and there is no point of ordering it, nor locking it
- delete @{$sub_attrs}{qw/collapse columns as select _prefetch_selector_range order_by for/};
+ delete @{$sub_attrs}{qw/collapse columns as select order_by for/};
# 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
->get_column ('count');
}
-sub _bool {
- return 1;
-}
=head2 count_literal
$self->throw_exception("all() doesn't take any arguments, you probably wanted ->search(...)->all()");
}
- delete @{$self}{qw/stashed_rows stashed_objects/};
+ delete @{$self}{qw/_stashed_rows _stashed_results/};
if (my $c = $self->get_cache) {
return @$c;
$self->cursor->reset;
- my $objs = $self->_construct_objects('fetch_all') || [];
+ my $objs = $self->_construct_results('fetch_all') || [];
$self->set_cache($objs) if $self->{attrs}{cache};
sub reset {
my ($self) = @_;
- delete @{$self}{qw/stashed_rows stashed_objects/};
+ delete @{$self}{qw/_stashed_rows _stashed_results/};
$self->{all_cache_position} = 0;
$self->cursor->reset;
return $self;
my $attrs = { %{$self->_resolved_attrs} };
my $join_classifications;
- my $existing_group_by = delete $attrs->{group_by};
+ my ($existing_group_by) = delete @{$attrs}{qw(group_by _grouped_by_distinct)};
# do we need a subquery for any reason?
my $needs_subq = (
# 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);
-
- # 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},
- $self->{cond},
- $attrs
- );
- # any non-pruneable joins imply subq
- $needs_subq = scalar keys %{ $join_classifications->{restricting} || {} };
- }
+ ($attrs->{from}, $join_classifications) =
+ $storage->_prune_unused_joins ($attrs);
+
+ # any non-pruneable non-local restricting joins imply subq
+ $needs_subq = defined List::Util::first { $_ ne $attrs->{alias} } keys %{ $join_classifications->{restricting} || {} };
}
# check if the head is composite (by now all joins are thrown out unless $needs_subq)
);
# 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/;
+ delete $attrs->{$_} for qw/select as collapse/;
$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
+
+ # this will be consumed by the pruner waaaaay down the stack
+ $attrs->{_force_prune_multiplying_joins} = 1;
+
my $subrs = (ref $self)->new($rsrc, $attrs);
if (@$idcols == 1) {
if (
$existing_group_by
or
+ # we do not need to check pre-multipliers, since if the premulti is there, its
+ # parent (who is multi) will be there too
keys %{ $join_classifications->{multiplying} || {} }
) {
# make sure if there is a supplied group_by it matches the columns compiled above
$guard = $storage->txn_scope_guard;
- $cond = [];
for my $row ($subrs->cursor->all) {
push @$cond, { map
{ $idcols->[$_] => $row->[$_] }
}
}
- my $res = $storage->$op (
+ my $res = $cond ? $storage->$op (
$rsrc,
$op eq 'update' ? $values : (),
$cond,
- );
+ ) : '0E0';
$guard->commit if $guard;
element should be a data value in the earlier specified column order.
For example:
- $Arstist_rs->populate([
+ $schema->resultset("Artist")->populate([
[ qw( artistid name ) ],
[ 100, 'A Formally Unknown Singer' ],
[ 101, 'A singer that jumped the shark two albums ago' ],
foreach my $rel (@rels) {
next unless ref $data->[$index]->{$rel} eq "HASH";
my $result = $self->related_resultset($rel)->create($data->[$index]->{$rel});
- my ($reverse_relname, $reverse_relinfo) = %{$rsrc->reverse_relationship_info($rel)};
+ my (undef, $reverse_relinfo) = %{$rsrc->reverse_relationship_info($rel)};
my $related = $result->result_source->_resolve_condition(
$reverse_relinfo->{cond},
$self,
}
-# populate() argumnets went over several incarnations
+# populate() arguments went over several incarnations
# What we ultimately support is AoH
sub _normalize_populate_args {
my ($self, $arg) = @_;
my ($merged_cond, $cols_from_relations) = $self->_merge_with_rscond($values);
- my %new = (
+ my $new = $self->result_class->new({
%$merged_cond,
- @$cols_from_relations
+ ( @$cols_from_relations
? (-cols_from_relations => $cols_from_relations)
- : (),
+ : ()
+ ),
-result_source => $self->result_source, # DO NOT REMOVE THIS, REQUIRED
- );
+ });
- return $self->result_class->new(\%new);
+ if (
+ reftype($new) eq 'HASH'
+ and
+ ! keys %$new
+ and
+ blessed($new)
+ ) {
+ carp_unique (sprintf (
+ "%s->new returned a blessed empty hashref - a strong indicator something is wrong with its inheritance chain",
+ $self->result_class,
+ ));
+ }
+
+ $new;
}
# _merge_with_rscond
);
}
else {
- # precendence must be given to passed values over values inherited from
- # the cond, so the order here is important.
- my $collapsed_cond = $self->_collapse_cond($self->{cond});
- my %implied = %{$self->_remove_alias($collapsed_cond, $alias)};
+ if ($self->{cond}) {
+ my $implied = $self->_remove_alias(
+ $self->result_source->schema->storage->_collapse_cond($self->{cond}),
+ $alias,
+ );
- while ( my($col, $value) = each %implied ) {
- my $vref = ref $value;
- if (
- $vref eq 'HASH'
- and
- keys(%$value) == 1
- and
- (keys %$value)[0] eq '='
- ) {
- $new_data{$col} = $value->{'='};
- }
- elsif( !$vref or $vref eq 'SCALAR' or blessed($value) ) {
- $new_data{$col} = $value;
+ for my $c (keys %$implied) {
+ my $v = $implied->{$c};
+ if ( ! length ref $v or is_plain_value($v) ) {
+ $new_data{$c} = $v;
+ }
+ elsif (
+ ref $v eq 'HASH' and keys %$v == 1 and exists $v->{'='} and is_literal_value($v->{'='})
+ ) {
+ $new_data{$c} = $v->{'='};
+ }
}
}
}
+ # precedence must be given to passed values over values inherited from
+ # the cond, so the order here is important.
%new_data = (
%new_data,
%{ $self->_remove_alias($data, $alias) },
# determines if the resultset defines at least one
# of the attributes supplied
#
-# used to determine if a subquery is neccessary
+# used to determine if a subquery is necessary
#
# supports some virtual attributes:
# -join
return 0;
}
-# _collapse_cond
-#
-# Recursively collapse the condition.
-
-sub _collapse_cond {
- my ($self, $cond, $collapsed) = @_;
-
- $collapsed ||= {};
-
- if (ref $cond eq 'ARRAY') {
- foreach my $subcond (@$cond) {
- next unless ref $subcond; # -or
- $collapsed = $self->_collapse_cond($subcond, $collapsed);
- }
- }
- elsif (ref $cond eq 'HASH') {
- if (keys %$cond and (keys %$cond)[0] eq '-and') {
- foreach my $subcond (@{$cond->{-and}}) {
- $collapsed = $self->_collapse_cond($subcond, $collapsed);
- }
- }
- else {
- foreach my $col (keys %$cond) {
- my $value = $cond->{$col};
- $collapsed->{$col} = $value;
- }
- }
- }
-
- return $collapsed;
-}
-
# _remove_alias
#
# Remove the specified alias from the specified query hash. A copy is made so
my $attrs = { %{ $self->_resolved_attrs } };
- # For future use:
- #
- # in list ctx:
- # my ($sql, \@bind, \%dbi_bind_attrs) = _select_args_to_query (...)
- # $sql also has no wrapping parenthesis in list ctx
- #
- my $sqlbind = $self->result_source->storage
- ->_select_args_to_query ($attrs->{from}, $attrs->{select}, $attrs->{where}, $attrs);
+ my $aq = $self->result_source->storage->_select_args_to_query (
+ $attrs->{from}, $attrs->{select}, $attrs->{where}, $attrs
+ );
- return $sqlbind;
+ $aq;
}
=head2 find_or_new
{ artist => 'fred' }, { key => 'artists' });
$cd->cd_to_producer->find_or_new({ producer => $producer },
- { key => 'primary });
+ { key => 'primary' });
Find an existing record from this resultset using L</find>. if none exists,
instantiate a new result object and return it. The object will not be saved
=cut
sub create {
- my ($self, $attrs) = @_;
+ my ($self, $col_data) = @_;
$self->throw_exception( "create needs a hashref" )
- unless ref $attrs eq 'HASH';
- return $self->new_result($attrs)->insert;
+ unless ref $col_data eq 'HASH';
+ return $self->new_result($col_data)->insert;
}
=head2 find_or_create
sub related_resultset {
my ($self, $rel) = @_;
- return $self->{related_resultsets}{$rel} ||= do {
+ return $self->{related_resultsets}{$rel}
+ if defined $self->{related_resultsets}{$rel};
+
+ return $self->{related_resultsets}{$rel} = do {
my $rsrc = $self->result_source;
my $rel_info = $rsrc->relationship_info($rel);
#XXX - temp fix for result_class bug. There likely is a more elegant fix -groditi
delete @{$attrs}{qw(result_class alias)};
- my $related_cache;
-
- if (my $cache = $self->get_cache) {
- $related_cache = [ map
- { @{$_->related_resultset($rel)->get_cache||[]} }
- @$cache
- ];
- }
-
my $rel_source = $rsrc->related_source($rel);
my $new = do {
where => $attrs->{where},
});
};
- $new->set_cache($related_cache) if $related_cache;
+
+ if (my $cache = $self->get_cache) {
+ my @related_cache = map
+ { @{$_->related_resultset($rel)->get_cache||[]} }
+ @$cache
+ ;
+
+ $new->set_cache(\@related_cache) if @related_cache;
+ }
+
$new;
};
}
# ->_resolve_join as otherwise they get lost - captainL
my $join = $self->_merge_joinpref_attr( $attrs->{join}, $attrs->{prefetch} );
- delete @{$attrs}{qw/join prefetch collapse group_by distinct select as columns +select +as +columns/};
+ delete @{$attrs}{qw/join prefetch collapse group_by distinct _grouped_by_distinct select as columns +select +as +columns/};
my $seen = { %{ (delete $attrs->{seen_join}) || {} } };
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};
my $source = $self->result_source;
my $alias = $attrs->{alias};
+ $self->throw_exception("Specifying distinct => 1 in conjunction with collapse => 1 is unsupported")
+ if $attrs->{collapse} and $attrs->{distinct};
+
# default selection list
$attrs->{columns} = [ $source->columns ]
unless List::Util::first { exists $attrs->{$_} } qw/columns cols select as/;
}
}
- # 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;
$attrs->{group_by} = [ $attrs->{group_by} ];
}
- # generate the distinct induced group_by early, as prefetch will be carried via a
- # subquery (since a group_by is present)
- if (delete $attrs->{distinct}) {
- if ($attrs->{group_by}) {
- carp_unique ("Useless use of distinct on a grouped resultset ('distinct' is ignored when a 'group_by' is present)");
- }
- else {
- # distinct affects only the main selection part, not what prefetch may
- # add below.
- $attrs->{group_by} = $source->storage->_group_over_selection (
- $attrs->{from},
- $attrs->{select},
- $attrs->{order_by},
- );
- }
- }
# generate selections based on the prefetch helper
- my $prefetch;
+ my ($prefetch, @prefetch_select, @prefetch_as);
$prefetch = $self->_merge_joinpref_attr( {}, delete $attrs->{prefetch} )
if defined $attrs->{prefetch};
$self->throw_exception("Unable to prefetch, resultset contains an unnamed selector $attrs->{_dark_selector}{string}")
if $attrs->{_dark_selector};
+ $self->throw_exception("Specifying prefetch in conjunction with an explicit collapse => 0 is unsupported")
+ if defined $attrs->{collapse} and ! $attrs->{collapse};
+
$attrs->{collapse} = 1;
# this is a separate structure (we don't look in {from} directly)
my @prefetch = $source->_resolve_prefetch( $prefetch, $alias, $join_map );
- # we need to somehow mark which columns came from prefetch
- if (@prefetch) {
- my $sel_end = $#{$attrs->{select}};
- $attrs->{_prefetch_selector_range} = [ $sel_end + 1, $sel_end + @prefetch ];
- }
-
- 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;
+ # save these for after distinct resolution
+ @prefetch_select = map { $_->[0] } @prefetch;
+ @prefetch_as = map { $_->[1] } @prefetch;
}
# run through the resulting joinstructure (starting from our current slot)
- # and unset collapse if proven unnesessary
+ # and unset collapse if proven unnecessary
#
# also while we are at it find out if the current root source has
# been premultiplied by previous related_source chaining
if (ref $attrs->{from} eq 'ARRAY') {
- if (@{$attrs->{from}} <= 1) {
+ if (@{$attrs->{from}} == 1) {
# no joins - no collapse
$attrs->{collapse} = 0;
}
}
}
+ # generate the distinct induced group_by before injecting the prefetched select/as parts
+ if (delete $attrs->{distinct}) {
+ if ($attrs->{group_by}) {
+ carp_unique ("Useless use of distinct on a grouped resultset ('distinct' is ignored when a 'group_by' is present)");
+ }
+ else {
+ $attrs->{_grouped_by_distinct} = 1;
+ # distinct affects only the main selection part, not what prefetch may add below
+ ($attrs->{group_by}, my $new_order) = $source->storage->_group_over_selection($attrs);
+
+ # FIXME possibly ignore a rewritten order_by (may turn out to be an issue)
+ # The thinking is: if we are collapsing the subquerying prefetch engine will
+ # rip stuff apart for us anyway, and we do not want to have a potentially
+ # function-converted external order_by
+ # ( there is an explicit if ( collapse && _grouped_by_distinct ) check in DBIHacks )
+ $attrs->{order_by} = $new_order unless $attrs->{collapse};
+ }
+ }
+
+ # inject prefetch-bound selection (if any)
+ push @{$attrs->{select}}, @prefetch_select;
+ push @{$attrs->{as}}, @prefetch_as;
+
+ # whether we can get away with the dumbest (possibly DBI-internal) collapser
+ if ( List::Util::first { $_ =~ /\./ } @{$attrs->{as}} ) {
+ $attrs->{_related_results_construction} = 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
expression). Adds C<me.> onto the start of any column without a C<.> in
it and sets C<select> from that, then auto-populates C<as> from
C<select> as normal. (You may also use the C<cols> attribute, as in
-earlier versions of DBIC.)
+earlier versions of DBIC, but this is deprecated.)
Essentially C<columns> does the same as L</select> and L</as>.
=back
-Indicates additional columns to be selected from storage. Works the same
-as L</columns> but adds columns to the selection. (You may also use the
-C<include_columns> attribute, as in earlier versions of DBIC). For
-example:-
+Indicates additional columns to be selected from storage. Works the same as
+L</columns> but adds columns to the selection. (You may also use the
+C<include_columns> attribute, as in earlier versions of DBIC, but this is
+deprecated). For example:-
$schema->resultset('CD')->search(undef, {
'+columns' => ['artist.name'],
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:
+example, the resultset:
my $rs = $schema->resultset('CD')->search({}, {
'+columns' => [ qw/ tracks.title tracks.position / ],
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<tracks>. 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.
+generated accessor C<tracks>. 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.
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.
If an L</order_by> 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<ORDER BY RANDOM()>) DBIC will automatically
-switch to "eager" mode and slurp the entire resultset before consturcting the
+has_many_rel.column >> or C<ORDER BY RANDOM()>), DBIC will automatically
+switch to "eager" mode and slurp the entire resultset before constructing the
first object returned by L</next>.
Setting this attribute on a resultset that does not join any has_many
relations is a no-op.
-For a more in depth discussion see L</PREFETCHING>.
+For a more in-depth discussion, see L</PREFETCHING>.
=head2 prefetch
This attribute is a shorthand for specifying a L</join> spec, adding all
columns from the joined related sources as L</+columns> and setting
-L</collapse> to a true value. For example the following two queries are
+L</collapse> to a true value. For example, the following two queries are
equivalent:
my $rs = $schema->resultset('Artist')->search({}, {
ON tracks.cd = cds.cdid
ORDER BY me.artistid
-While L</prefetch> implies a L</join> it is ok to mix the two together, as
+While L</prefetch> implies a L</join>, 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:
+example, you may want to do the following:
my $artists_and_cds_without_genre = $schema->resultset('Artist')->search(
{ 'genre.genreid' => undef },
WHERE genre.genreid IS NULL
ORDER BY me.artistid
-For a more in depth discussion see L</PREFETCHING>.
+For a more in-depth discussion, see L</PREFETCHING>.
=head2 alias
=back
-Set to 1 to group by all columns. If the resultset already has a group_by
-attribute, this setting is ignored and an appropriate warning is issued.
+Set to 1 to automatically generate a L</group_by> clause based on the selection
+(including intelligent handling of L</order_by> contents). Note that the group
+criteria calculation takes place over the B<final> selection. This includes
+any L</+columns>, L</+select> or L</order_by> additions in subsequent
+L</search> calls, and standalone columns selected via
+L<DBIx::Class::ResultSetColumn> (L</get_column>). A notable exception are the
+extra selections specified via L</prefetch> - such selections are explicitly
+excluded from group criteria calculations.
+
+If the final ResultSet also explicitly defines a L</group_by> attribute, this
+setting is ignored and an appropriate warning is issued.
=head2 where
=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<collapsing|/collapse> is requested there is an additional requirement of
+sources. Any combination of relationship types and column sets are supported.
+If L<collapsing|/collapse> 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' );
+ # 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::Artist->belongs_to( record_label => 'My::Schema::RecordLabel' );
- My::Schema::Track->has_many( guests => 'My::Schema::Guest' );
+ My::Schema::Track->has_many( guests => 'My::Schema::Guest' );
Simple prefetches will be joined automatically, so there is no need
for a C<join> attribute in the above search.
-L</prefetch> can be used with any of the relationship types and
-multiple prefetches can be specified together. Below is a more complex
+The L</prefetch> 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
+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
- ]
- }
- );
+ 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
+ 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<artist>, C<record_label>, C<liner_note>, C<cover_image>,
C<tracks>, and C<guests> 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
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.
+That cmp_ok() may or may not pass depending on the datasets involved. In other
+words the C<WHERE> condition would apply to the entire dataset, just like
+it would in regular SQL. If you want to add a condition only to the "right side"
+of a C<LEFT JOIN> - consider declaring and using a L<relationship with a custom
+condition|DBIx::Class::Relationship::Base/condition>
=back
=item dbic_colname
Used to fill in missing sqlt_datatype and sqlt_size attributes (if they are
-explicitly specified they are never overriden). Also used by some weird DBDs,
+explicitly specified they are never overridden). Also used by some weird DBDs,
where the column name should be available at bind_param time (e.g. Oracle).
=back
[ $name => $val ] === [ { dbic_colname => $name }, $val ]
[ \$dt => $val ] === [ { sqlt_datatype => $dt }, $val ]
[ undef, $val ] === [ {}, $val ]
+ $val === [ {}, $val ]
=head1 AUTHOR AND CONTRIBUTORS