my $unique_query = $self->_build_unique_query($input_query, \@unique_cols);
$query = $self->_add_alias($unique_query, $alias);
}
+ elsif ($self->{attrs}{accessor} and $self->{attrs}{accessor} eq 'single') {
+ # This means that we got here after a merger of relationship conditions
+ # in ::Relationship::Base::search_related (the row method), and furthermore
+ # the relationship is of the 'single' type. This means that the condition
+ # provided by the relationship (already attached to $self) is sufficient,
+ # as there can be only one row in the databse that would satisfy the
+ # relationship
+ }
else {
my @unique_queries = $self->_unique_queries($input_query, $attrs);
$query = @unique_queries
}
# Run the query
- if (keys %$attrs) {
- my $rs = $self->search($query, $attrs);
- if (keys %{$rs->_resolved_attrs->{collapse}}) {
- my $row = $rs->next;
- carp "Query returned more than one row" if $rs->next;
- return $row;
- }
- else {
- return $rs->single;
- }
+ my $rs = $self->search ($query, $attrs);
+ if (keys %{$rs->_resolved_attrs->{collapse}}) {
+ my $row = $rs->next;
+ carp "Query returned more than one row" if $rs->next;
+ return $row;
}
else {
- if (keys %{$self->_resolved_attrs->{collapse}}) {
- my $rs = $self->search($query);
- my $row = $rs->next;
- carp "Query returned more than one row" if $rs->next;
- return $row;
- }
- else {
- return $self->single($query);
- }
+ return $rs->single;
}
}
Query returned more than one row
-In this case, you should be using L</first> or L</find> instead, or if you really
+In this case, you should be using L</next> or L</find> instead, or if you really
know what you are doing, use the L</rows> attribute to explicitly limit the size
of the resultset.
+This method will also throw an exception if it is called on a resultset prefetching
+has_many, as such a prefetch implies fetching multiple rows from the database in
+order to assemble the resulting object.
+
=back
=cut
my $attrs = $self->_resolved_attrs_copy;
+ if (keys %{$attrs->{collapse}}) {
+ $self->throw_exception(
+ 'single() can not be used on resultsets prefetching has_many. Use find( \%cond ) or next() instead'
+ );
+ }
+
if ($where) {
if (defined $attrs->{where}) {
$attrs->{where} = {
sub _construct_object {
my ($self, @row) = @_;
- my $info = $self->_collapse_result($self->{_attrs}{as}, \@row);
+
+ 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};
sub _collapse_result {
my ($self, $as_proto, $row) = @_;
+ # if the first row that ever came in is totally empty - this means we got
+ # hit by a smooth^Wempty left-joined resultset. Just noop in that case
+ # instead of producing a {}
+ #
+ my $has_def;
+ for (@$row) {
+ if (defined $_) {
+ $has_def++;
+ last;
+ }
+ }
+ return undef unless $has_def;
+
my @copy = @$row;
# 'foo' => [ undef, 'foo' ]
return $self->search(@_)->count if @_ and defined $_[0];
return scalar @{ $self->get_cache } if $self->get_cache;
- my $meth = $self->_has_attr (qw/prefetch collapse distinct group_by/)
- ? 'count_grouped'
- : 'count'
- ;
-
my $attrs = $self->_resolved_attrs_copy;
+
+ # 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 $crs;
+ if ($self->_has_resolved_attr (qw/collapse group_by/)) {
+ $crs = $self->_count_subq_rs ($attrs);
+ }
+ else {
+ $crs = $self->_count_rs ($attrs);
+ }
+ my $count = $crs->next;
+
+ $count -= $offset if $offset;
+ $count = $rows if $rows and $rows < $count;
+ $count = 0 if ($count < 0);
+
+ return $count;
+}
+
+=head2 count_rs
+
+=over 4
+
+=item Arguments: $cond, \%attrs??
+
+=item Return Value: $count_rs
+
+=back
+
+Same as L</count> but returns a L<DBIx::Class::ResultSetColumn> object.
+This can be very handy for subqueries:
+
+ ->search( { amount => $some_rs->count_rs->as_query } )
+
+As with regular resultsets the SQL query will be executed only after
+the resultset is accessed via L</next> or L</all>. That would return
+the same single value obtainable via L</count>.
+
+=cut
+
+sub count_rs {
+ my $self = shift;
+ return $self->search(@_)->count_rs if @_;
+
+ # this may look like a lack of abstraction (count() does about the same)
+ # but in fact an _rs *must* use a subquery for the limits, as the
+ # 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;
+ }
+ else {
+ return $self->_count_rs;
+ }
+}
+
+#
+# returns a ResultSetColumn object tied to the count query
+#
+sub _count_rs {
+ my ($self, $attrs) = @_;
+
my $rsrc = $self->result_source;
+ $attrs ||= $self->_resolved_attrs;
- return $rsrc->storage->$meth ($rsrc, $attrs);
+ my $tmp_attrs = { %$attrs };
+
+ # take off any limits, record_filter is cdbi, and no point of ordering a count
+ delete $tmp_attrs->{$_} for (qw/select as rows offset order_by record_filter/);
+
+ # overwrite the selector (supplied by the storage)
+ $tmp_attrs->{select} = $rsrc->storage->_count_select ($rsrc, $tmp_attrs);
+ $tmp_attrs->{as} = 'count';
+
+ # read the comment on top of the actual function to see what this does
+ $tmp_attrs->{from} = $self->_switch_to_inner_join_if_needed (
+ $tmp_attrs->{from}, $tmp_attrs->{alias}
+ );
+
+ my $tmp_rs = $rsrc->resultset_class->new($rsrc, $tmp_attrs)->get_column ('count');
+
+ return $tmp_rs;
}
+#
+# same as above but uses a subquery
+#
+sub _count_subq_rs {
+ my ($self, $attrs) = @_;
+
+ my $rsrc = $self->result_source;
+ $attrs ||= $self->_resolved_attrs_copy;
+
+ my $sub_attrs = { %$attrs };
+
+ # extra selectors do not go in the subquery and there is no point of ordering it
+ delete $sub_attrs->{$_} for qw/collapse prefetch_select select as order_by/;
+
+ # if we prefetch, we group_by primary keys only as this is what we would get out of the rs via ->next/->all
+ # clobber old group_by regardless
+ if ( keys %{$attrs->{collapse}} ) {
+ $sub_attrs->{group_by} = [ map { "$attrs->{alias}.$_" } ($rsrc->primary_columns) ]
+ }
+
+ $sub_attrs->{select} = $rsrc->storage->_subq_count_select ($rsrc, $sub_attrs);
+
+ # read the comment on top of the actual function to see what this does
+ $sub_attrs->{from} = $self->_switch_to_inner_join_if_needed (
+ $sub_attrs->{from}, $sub_attrs->{alias}
+ );
+
+ # this is so that ordering can be thrown away in things like Top limit
+ $sub_attrs->{-for_count_only} = 1;
+
+ my $sub_rs = $rsrc->resultset_class->new ($rsrc, $sub_attrs);
+
+ $attrs->{from} = [{
+ -alias => 'count_subq',
+ -source_handle => $rsrc->handle,
+ count_subq => $sub_rs->as_query,
+ }];
+
+ # the subquery replaces this
+ delete $attrs->{$_} for qw/where bind collapse group_by having having_bind rows offset/;
+
+ return $self->_count_rs ($attrs);
+}
+
+
+# The DBIC relationship chaining implementation is pretty simple - every
+# new related_relationship is pushed onto the {from} stack, and the {select}
+# window simply slides further in. This means that when we count somewhere
+# in the middle, we got to make sure that everything in the join chain is an
+# actual inner join, otherwise the count will come back with unpredictable
+# results (a resultset may be generated with _some_ rows regardless of if
+# the relation which the $rs currently selects has rows or not). E.g.
+# $artist_rs->cds->count - normally generates:
+# SELECT COUNT( * ) FROM artist me LEFT JOIN cd cds ON cds.artist = me.artistid
+# which actually returns the number of artists * (number of cds || 1)
+#
+# So what we do here is crawl {from}, determine if the current alias is at
+# the top of the stack, and if not - make sure the chain is inner-joined down
+# to the root.
+#
+sub _switch_to_inner_join_if_needed {
+ my ($self, $from, $alias) = @_;
+
+ # subqueries and other oddness is naturally not supported
+ return $from if (
+ ref $from ne 'ARRAY'
+ ||
+ @$from <= 1
+ ||
+ ref $from->[0] ne 'HASH'
+ ||
+ ! $from->[0]{-alias}
+ ||
+ $from->[0]{-alias} eq $alias
+ );
+
+ my $switch_branch;
+ JOINSCAN:
+ for my $j (@{$from}[1 .. $#$from]) {
+ if ($j->[0]{-alias} eq $alias) {
+ $switch_branch = $j->[0]{-join_path};
+ last JOINSCAN;
+ }
+ }
+
+ # something else went wrong
+ return $from unless $switch_branch;
+
+ # So it looks like we will have to switch some stuff around.
+ # local() is useless here as we will be leaving the scope
+ # anyway, and deep cloning is just too fucking expensive
+ # So replace the inner hashref manually
+ my @new_from = ($from->[0]);
+ my $sw_idx = { map { $_ => 1 } @$switch_branch };
+
+ for my $j (@{$from}[1 .. $#$from]) {
+ my $jalias = $j->[0]{-alias};
+
+ if ($sw_idx->{$jalias}) {
+ my %attrs = %{$j->[0]};
+ delete $attrs{-join_type};
+ push @new_from, [
+ \%attrs,
+ @{$j}[ 1 .. $#$j ],
+ ];
+ }
+ else {
+ push @new_from, $j;
+ }
+ }
+
+ return \@new_from;
+}
+
+
sub _bool {
return 1;
}
my @obj;
- # TODO: don't call resolve here
if (keys %{$self->_resolved_attrs->{collapse}}) {
-# if ($self->{attrs}{prefetch}) {
- # 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
+ # 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));
}
$self->set_cache(\@obj) if $self->{attrs}{cache};
+
return @obj;
}
=back
Resets the resultset's cursor, so you can iterate through the elements again.
+Implicitly resets the storage cursor, so a subsequent L</next> will trigger
+another query.
=cut
my $rsrc = $self->result_source;
- my $needs_group_by_subq = $self->_has_attr (qw/prefetch distinct join seen_join group_by/);
- my $needs_subq = $self->_has_attr (qw/row offset page/);
+ my $needs_group_by_subq = $self->_has_resolved_attr (qw/collapse group_by -join/);
+ my $needs_subq = $self->_has_resolved_attr (qw/row offset/);
if ($needs_group_by_subq or $needs_subq) {
# make a new $rs selecting only the PKs (that's all we really need)
my $attrs = $self->_resolved_attrs_copy;
- delete $attrs->{$_} for qw/prefetch collapse select +select as +as columns +columns/;
+ delete $attrs->{$_} for qw/collapse select as/;
$attrs->{columns} = [ map { "$attrs->{alias}.$_" } ($self->result_source->primary_columns) ];
if ($needs_group_by_subq) {
to insert the data, as this is a faster method.
Otherwise, each set of data is inserted into the database using
-L<DBIx::Class::ResultSet/create>, and a arrayref of the resulting row
-objects is returned.
+L<DBIx::Class::ResultSet/create>, and the resulting objects are
+accumulated into an array. The array itself, or an array reference
+is returned depending on scalar or list context.
Example: Assuming an Artist Class that has many CDs Classes relating:
foreach my $item (@$data) {
push(@created, $self->create($item));
}
- return @created;
+ return wantarray ? @created : \@created;
} else {
my ($first, @rest) = @$data;
return 0;
}
-# _has_attr
+# _has_resolved_attr
#
# determines if the resultset defines at least one
# of the attributes supplied
#
# used to determine if a subquery is neccessary
+#
+# supports some virtual attributes:
+# -join
+# This will scan for any joins being present on the resultset.
+# It is not a mere key-search but a deep inspection of {from}
+#
-sub _has_attr {
+sub _has_resolved_attr {
my ($self, @attr_names) = @_;
my $attrs = $self->_resolved_attrs;
- my $join_check_req;
+ my %extra_checks;
for my $n (@attr_names) {
- ++$join_check_req if $n =~ /join/;
+ if (grep { $n eq $_ } (qw/-join/) ) {
+ $extra_checks{$n}++;
+ next;
+ }
my $attr = $attrs->{$n};
}
}
- # a join can be expressed as a multi-level from
+ # a resolved join is expressed as a multi-level from
return 1 if (
- $join_check_req
+ $extra_checks{-join}
and
ref $attrs->{from} eq 'ARRAY'
and
can also be passed an object representing the foreign row, and the
value will be set to its primary key.
-To create related objects, pass a hashref for the value if the related
-item is a foreign key relationship (L<DBIx::Class::Relationship/belongs_to>),
-and use the name of the relationship as the key. (NOT the name of the field,
-necessarily). For C<has_many> and C<has_one> relationships, pass an arrayref
-of hashrefs containing the data for each of the rows to create in the foreign
-tables, again using the relationship name as the key.
+To create related objects, pass a hashref of related-object column values
+B<keyed on the relationship name>. If the relationship is of type C<multi>
+(L<DBIx::Class::Relationship/has_many>) - pass an arrayref of hashrefs.
+The process will correctly identify columns holding foreign keys, and will
+transparrently populate them from the keys of the corresponding relation.
+This can be applied recursively, and will work correctly for a structure
+with an arbitrary depth and width, as long as the relationships actually
+exists and the correct column data has been supplied.
+
Instead of hashrefs of plain related data (key/value pairs), you may
also pass new or inserted objects. New objects (not inserted yet, see
=back
$cd->cd_to_producer->find_or_create({ producer => $producer },
- { key => 'primary });
+ { key => 'primary' });
Tries to find a record based on its primary key or unique constraints; if none
is found, creates one and returns that instead.
$self->{related_resultsets} ||= {};
return $self->{related_resultsets}{$rel} ||= do {
- my $rel_obj = $self->result_source->relationship_info($rel);
+ my $rel_info = $self->result_source->relationship_info($rel);
$self->throw_exception(
"search_related: result source '" . $self->result_source->source_name .
"' has no such relationship $rel")
- unless $rel_obj;
+ unless $rel_info;
- my ($from,$seen) = $self->_resolve_from($rel);
+ my ($from,$seen) = $self->_chain_relationship($rel);
my $join_count = $seen->{$rel};
my $alias = ($join_count > 1 ? join('_', $rel, $join_count) : $rel);
# in order to properly resolve prefetch aliases (any alias
# with a relation_chain_depth less than the depth of the
# current prefetch is not considered)
-sub _resolve_from {
- my ($self, $extra_join) = @_;
+#
+# The increments happen in 1/2s to make it easier to correlate the
+# join depth with the join path. An integer means a relationship
+# specified via a search_related, whereas a fraction means an added
+# join/prefetch via attributes
+sub _chain_relationship {
+ my ($self, $rel) = @_;
my $source = $self->result_source;
my $attrs = $self->{attrs};
}];
my $seen = { %{$attrs->{seen_join} || {} } };
+ my $jpath = ($attrs->{seen_join} && keys %{$attrs->{seen_join}})
+ ? $from->[-1][0]{-join_path}
+ : [];
+
# we need to take the prefetch the attrs into account before we
# ->_resolve_join as otherwise they get lost - captainL
my $merged = $self->_merge_attr( $attrs->{join}, $attrs->{prefetch} );
- push @$from, $source->_resolve_join($merged, $attrs->{alias}, $seen) if ($merged);
+ my @requested_joins = $source->_resolve_join(
+ $merged,
+ $attrs->{alias},
+ $seen,
+ $jpath,
+ );
- ++$seen->{-relation_chain_depth};
+ push @$from, @requested_joins;
- push @$from, $source->_resolve_join($extra_join, $attrs->{alias}, $seen);
+ $seen->{-relation_chain_depth} += 0.5;
- ++$seen->{-relation_chain_depth};
+ # if $self already had a join/prefetch specified on it, the requested
+ # $rel might very well be already included. What we do in this case
+ # is effectively a no-op (except that we bump up the chain_depth on
+ # the join in question so we could tell it *is* the search_related)
+ my $already_joined;
+
+
+ # we consider the last one thus reverse
+ for my $j (reverse @requested_joins) {
+ if ($rel eq $j->[0]{-join_path}[-1]) {
+ $j->[0]{-relation_chain_depth} += 0.5;
+ $already_joined++;
+ last;
+ }
+ }
+
+# alternative way to scan the entire chain - not backwards compatible
+# for my $j (reverse @$from) {
+# next unless ref $j eq 'ARRAY';
+# if ($j->[0]{-join_path} && $j->[0]{-join_path}[-1] eq $rel) {
+# $j->[0]{-relation_chain_depth} += 0.5;
+# $already_joined++;
+# last;
+# }
+# }
+
+ unless ($already_joined) {
+ push @$from, $source->_resolve_join(
+ $rel,
+ $attrs->{alias},
+ $seen,
+ $jpath,
+ );
+ }
+
+ $seen->{-relation_chain_depth} += 0.5;
return ($from,$seen);
}
$self->{attrs}{alias} => $source->from,
} ];
- if ( exists $attrs->{join} || exists $attrs->{prefetch} ) {
+ if ( $attrs->{join} || $attrs->{prefetch} ) {
+
+ $self->throw_exception ('join/prefetch can not be used with a literal scalarref {from}')
+ if ref $attrs->{from} ne 'ARRAY';
+
my $join = delete $attrs->{join} || {};
if ( defined $attrs->{prefetch} ) {
$join = $self->_merge_attr( $join, $attrs->{prefetch} );
-
}
$attrs->{from} = # have to copy here to avoid corrupting the original
[
- @{ $attrs->{from} },
- $source->_resolve_join(
- $join, $alias, { %{ $attrs->{seen_join} || {} } }
- )
+ @{ $attrs->{from} },
+ $source->_resolve_join(
+ $join,
+ $alias,
+ { %{ $attrs->{seen_join} || {} } },
+ ($attrs->{seen_join} && keys %{$attrs->{seen_join}})
+ ? $attrs->{from}[-1][0]{-join_path}
+ : []
+ ,
+ )
];
-
}
- if ( $attrs->{order_by} ) {
+ if ( defined $attrs->{order_by} ) {
$attrs->{order_by} = (
ref( $attrs->{order_by} ) eq 'ARRAY'
? [ @{ $attrs->{order_by} } ]
- : [ $attrs->{order_by} ]
+ : [ $attrs->{order_by} || () ]
);
}
- else {
- $attrs->{order_by} = [];
- }
-
- # If the order_by is otherwise empty - we will use this for TOP limit
- # emulation and the like.
- # Although this is needed only if the order_by is not defined, it is
- # actually cheaper to just populate this rather than properly examining
- # order_by (stuf like [ {} ] and the like)
- $attrs->{_virtual_order_by} = [ $self->result_source->primary_columns ];
+ if ($attrs->{group_by} and ! ref $attrs->{group_by}) {
+ $attrs->{group_by} = [ $attrs->{group_by} ];
+ }
- my $collapse = $attrs->{collapse} || {};
-
+ $attrs->{collapse} ||= {};
if ( my $prefetch = delete $attrs->{prefetch} ) {
$prefetch = $self->_merge_attr( {}, $prefetch );
my $join_map = $self->_joinpath_aliases ($attrs->{from}, $attrs->{seen_join});
my @prefetch =
- $source->_resolve_prefetch( $prefetch, $alias, $join_map, $prefetch_ordering, $collapse );
+ $source->_resolve_prefetch( $prefetch, $alias, $join_map, $prefetch_ordering, $attrs->{collapse} );
- push( @{ $attrs->{select} }, map { $_->[0] } @prefetch );
- push( @{ $attrs->{as} }, map { $_->[1] } @prefetch );
+ $attrs->{prefetch_select} = [ map { $_->[0] } @prefetch ];
+ push @{ $attrs->{select} }, @{$attrs->{prefetch_select}};
+ push @{ $attrs->{as} }, (map { $_->[1] } @prefetch);
- push( @{ $attrs->{order_by} }, @$prefetch_ordering );
+ push( @{$attrs->{order_by}}, @$prefetch_ordering );
+ $attrs->{_collapse_order_by} = \@$prefetch_ordering;
}
+
if (delete $attrs->{distinct}) {
$attrs->{group_by} ||= [ grep { !ref($_) || (ref($_) ne 'HASH') } @{$attrs->{select}} ];
}
- $attrs->{collapse} = $collapse;
-
- if ( $attrs->{page} and not defined $attrs->{offset} ) {
- $attrs->{offset} = ( $attrs->{rows} * ( $attrs->{page} - 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
+ if (my $page = delete $attrs->{page}) {
+ $attrs->{offset} =
+ ($attrs->{rows} * ($page - 1))
+ +
+ ($attrs->{offset} || 0)
+ ;
}
return $self->{_attrs} = $attrs;
my $paths = {};
return $paths unless ref $fromspec eq 'ARRAY';
+ my $cur_depth = $seen->{-relation_chain_depth} || 0;
+
+ if (int ($cur_depth) != $cur_depth) {
+ $self->throw_exception ("-relation_chain_depth is not an integer, something went horribly wrong ($cur_depth)");
+ }
+
for my $j (@$fromspec) {
next if ref $j ne 'ARRAY';
- next if $j->[0]{-relation_chain_depth} < ( $seen->{-relation_chain_depth} || 0);
+ next if $j->[0]{-relation_chain_depth} < $cur_depth;
+
+ my $jpath = $j->[0]{-join_path};
my $p = $paths;
- $p = $p->{$_} ||= {} for @{$j->[0]{-join_path}};
+ $p = $p->{$_} ||= {} for @{$jpath}[$cur_depth .. $#$jpath];
push @{$p->{-join_aliases} }, $j->[0]{-alias};
}
=back
-Which column(s) to order the results by. If a single column name, or
-an arrayref of names is supplied, the argument is passed through
-directly to SQL. The hashref syntax allows for connection-agnostic
-specification of ordering direction:
+Which column(s) to order the results by.
+
+[The full list of suitable values is documented in
+L<SQL::Abstract/"ORDER BY CLAUSES">; the following is a summary of
+common options.]
+
+If a single column name, or an arrayref of names is supplied, the
+argument is passed through directly to SQL. The hashref syntax allows
+for connection-agnostic specification of ordering direction:
For descending order:
identical to creating a non-pages resultset and then calling ->page($page)
on it.
-If L<rows> attribute is not specified it defualts to 10 rows per page.
+If L<rows> attribute is not specified it defaults to 10 rows per page.
When you have a paged resultset, L</count> will only return the number
of rows in the page. To get the total, use the L</pager> and call