X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FResultSet.pm;h=f0a57906b8e8f200a512289d37ae16762524ad9c;hb=c2eb0f228395591bcdcb7d4254c890104bd42fce;hp=13e11744ffd256e0d5f95847ac1c233dbf810e18;hpb=639cf8f9d212c3e950ae5bae680133762c1df0aa;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/ResultSet.pm b/lib/DBIx/Class/ResultSet.pm index 13e1174..f0a5790 100644 --- a/lib/DBIx/Class/ResultSet.pm +++ b/lib/DBIx/Class/ResultSet.pm @@ -46,27 +46,13 @@ A new ResultSet is returned from calling L on an existing ResultSet. The new one will contain all the conditions of the original, plus any new conditions added in the C call. -A ResultSet is also an iterator. L is used to return all the -Ls the ResultSet represents. +A ResultSet also incorporates an implicit iterator. L and L +can be used to walk through all the Ls the ResultSet +represents. The query that the ResultSet represents is B executed against the database when these methods are called: - -=over - -=item L - -=item L - -=item L - -=item L - -=item L - -=item L - -=back +L L L L L L =head1 EXAMPLES @@ -674,7 +660,8 @@ L for more information. sub cursor { my ($self) = @_; - my $attrs = { %{$self->_resolved_attrs} }; + my $attrs = $self->_resolved_attrs_copy; + return $self->{cursor} ||= $self->result_source->storage->select($attrs->{from}, $attrs->{select}, $attrs->{where},$attrs); @@ -711,10 +698,14 @@ a warning: Query returned more than one row -In this case, you should be using L or L instead, or if you really +In this case, you should be using L or L instead, or if you really know what you are doing, use the L 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 @@ -725,7 +716,14 @@ sub single { $self->throw_exception('single() only takes search conditions, no attributes. You want ->search( $cond, $attrs )->single()'); } - my $attrs = { %{$self->_resolved_attrs} }; + 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} = { @@ -752,6 +750,7 @@ sub single { return (@data ? ($self->_construct_object(@data))[0] : undef); } + # _is_unique_query # # Try to determine if the specified query is guaranteed to be unique, based on @@ -870,10 +869,10 @@ instead. An example conversion is: sub search_like { my $class = shift; - carp join ("\n", - 'search_like() is deprecated and will be removed in 0.09.', - 'Instead use ->search({ x => { -like => "y%" } })', - '(note the outer pair of {}s - they are important!)' + carp ( + 'search_like() is deprecated and will be removed in DBIC version 0.09.' + .' Instead use ->search({ x => { -like => "y%" } })' + .' (note the outer pair of {}s - they are important!)' ); my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); my $query = ref $_[0] eq 'HASH' ? { %{shift()} }: {@_}; @@ -1145,8 +1144,8 @@ sub result_class { =back Performs an SQL C with the same query as the resultset was built -with to find the number of elements. If passed arguments, does a search -on the resultset and counts the results of that. +with to find the number of elements. Passing arguments is equivalent to +C<< $rs->search ($cond, \%attrs)->count >> =cut @@ -1155,75 +1154,122 @@ sub count { return $self->search(@_)->count if @_ and defined $_[0]; return scalar @{ $self->get_cache } if $self->get_cache; - my @subq_attrs = qw/prefetch collapse distinct group_by having having_bind/; - my $attrs = $self->_resolved_attrs; + 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}; - # if we are not paged - we are simply asking for a limit - if (not $attrs->{page} and not $attrs->{software_limit}) { - push @subq_attrs, qw/rows 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 $self->_has_attr (@subq_attrs) - ? $self->_count_subq - : $self->_count_simple + return $count; } -sub _count_subq { - my $self = shift; +=head2 count_rs - my $attrs = { %{$self->_resolved_attrs} }; +=over 4 - # copy for the subquery, we need to do some adjustments to it too - my $sub_attrs = { %$attrs }; +=item Arguments: $cond, \%attrs?? - # these can not go in the subquery either - delete $sub_attrs->{$_} for qw/prefetch select +select as +as columns +columns/; +=item Return Value: $count_rs - # force a group_by and the same set of columns (most databases require this) - $sub_attrs->{columns} = $sub_attrs->{group_by} ||= [ map { "$attrs->{alias}.$_" } ($self->result_source->primary_columns) ]; +=back - $attrs->{from} = [{ - count_subq => (ref $self)->new ($self->result_source, $sub_attrs )->as_query - }]; +Same as L but returns a L object. +This can be very handy for subqueries: - # the subquery replaces this - delete $attrs->{$_} for qw/where bind prefetch collapse group_by having/; + ->search( { amount => $some_rs->count_rs->as_query } ) - return $self->__count ($attrs); -} +As with regular resultsets the SQL query will be executed only after +the resultset is accessed via L or L. That would return +the same single value obtainable via L. + +=cut -sub _count_simple { +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; + } +} - my $count = $self->__count; - return 0 unless $count; +# +# returns a ResultSetColumn object tied to the count query +# +sub _count_rs { + my ($self, $attrs) = @_; - # need to take offset from resolved attrs + my $rsrc = $self->result_source; + $attrs ||= $self->_resolved_attrs; - $count -= $self->{_attrs}{offset} if $self->{_attrs}{offset}; - $count = $self->{attrs}{rows} if - $self->{attrs}{rows} and $self->{attrs}{rows} < $count; - $count = 0 if ($count < 0); - return $count; + 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'; + + my $tmp_rs = $rsrc->resultset_class->new($rsrc, $tmp_attrs)->get_column ('count'); + + return $tmp_rs; } -sub __count { +# +# same as above but uses a subquery +# +sub _count_subq_rs { my ($self, $attrs) = @_; - $attrs ||= { %{$self->_resolved_attrs} }; + my $rsrc = $self->result_source; + $attrs ||= $self->_resolved_attrs_copy; - # take off any column specs, any pagers, record_filter is cdbi, and no point of ordering a count - delete $attrs->{$_} for (qw/columns +columns select +select as +as rows offset page pager order_by record_filter/); + my $sub_attrs = { %$attrs }; - $attrs->{select} = { count => '*' }; - $attrs->{as} = [qw/count/]; + # these can not go in the subquery, and there is no point of ordering it + delete $sub_attrs->{$_} for qw/collapse select as order_by/; - my $tmp_rs = (ref $self)->new($self->result_source, $attrs); - my ($count) = $tmp_rs->cursor->next; + # 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) ] + } - return $count; + $sub_attrs->{select} = $rsrc->storage->_subq_count_select ($rsrc, $sub_attrs); + + $attrs->{from} = [{ + count_subq => $rsrc->resultset_class->new ($rsrc, $sub_attrs )->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); } + sub _bool { return 1; } @@ -1345,15 +1391,15 @@ sub _rs_update_delete { 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; + my $attrs = $self->_resolved_attrs_copy; - delete $attrs->{$_} for qw/prefetch 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) { @@ -1387,7 +1433,7 @@ sub _rs_update_delete { my $subrs = (ref $self)->new($rsrc, $attrs); - return $self->result_source->storage->subq_update_delete($subrs, $op, $values); + return $self->result_source->storage->_subq_update_delete($subrs, $op, $values); } else { return $rsrc->storage->$op( @@ -1512,7 +1558,7 @@ sub update_all { =item Arguments: none -=item Return Value: 1 +=item Return Value: $storage_rv =back @@ -1520,11 +1566,8 @@ Deletes the contents of the resultset from its result source. Note that this will not run DBIC cascade triggers. See L if you need triggers to run. See also L. -delete may not generate correct SQL for a query with joins or a resultset -chained from a related resultset. In this case it will generate a warning:- - -In these cases you may find that delete_all is more appropriate, or you -need to respecify your query in a way that can be expressed without a join. +Return value will be the amount of rows deleted; exact type of return value +is storage-dependent. =cut @@ -1576,8 +1619,9 @@ In void context, C in L is used to insert the data, as this is a faster method. Otherwise, each set of data is inserted into the database using -L, and a arrayref of the resulting row -objects is returned. +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. Example: Assuming an Artist Class that has many CDs Classes relating: @@ -1643,7 +1687,7 @@ sub populate { foreach my $item (@$data) { push(@created, $self->create($item)); } - return @created; + return wantarray ? @created : \@created; } else { my ($first, @rest) = @$data; @@ -1653,13 +1697,19 @@ sub populate { ## do the belongs_to relationships foreach my $index (0..$#$data) { - if( grep { !defined $data->[$index]->{$_} } @pks ) { - my @ret = $self->populate($data); - return; + + # delegate to create() for any dataset without primary keys with specified relationships + if (grep { !defined $data->[$index]->{$_} } @pks ) { + for my $r (@rels) { + if (grep { ref $data->[$index]{$r} eq $_ } qw/HASH ARRAY/) { # a related set must be a HASH or AoH + my @ret = $self->populate($data); + return; + } + } } foreach my $rel (@rels) { - next unless $data->[$index]->{$rel} && ref $data->[$index]->{$rel} eq "HASH"; + next unless ref $data->[$index]->{$rel} eq "HASH"; my $result = $self->related_resultset($rel)->create($data->[$index]->{$rel}); my ($reverse) = keys %{$self->result_source->reverse_relationship_info($rel)}; my $related = $result->result_source->_resolve_condition( @@ -1874,14 +1924,14 @@ sub _is_deterministic_value { 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 -sub _has_attr { +sub _has_resolved_attr { my ($self, @attr_names) = @_; my $attrs = $self->_resolved_attrs; @@ -1889,7 +1939,7 @@ sub _has_attr { my $join_check_req; for my $n (@attr_names) { - ++$join_check_req if $n =~ /join/; + ++$join_check_req if $n eq '-join'; my $attr = $attrs->{$n}; @@ -1906,7 +1956,7 @@ sub _has_attr { } } - # 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 and @@ -1991,7 +2041,22 @@ B: This feature is still experimental. =cut -sub as_query { return shift->cursor->as_query(@_) } +sub as_query { + my $self = shift; + + my $attrs = $self->_resolved_attrs_copy; + + # 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); + + return $sqlbind; +} =head2 find_or_new @@ -2032,8 +2097,10 @@ sub find_or_new { my $self = shift; my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); my $hash = ref $_[0] eq 'HASH' ? shift : {@_}; - my $exists = $self->find($hash, $attrs); - return defined $exists ? $exists : $self->new_result($hash); + if (keys %$hash and my $row = $self->find($hash, $attrs) ) { + return $row; + } + return $self->new_result($hash); } =head2 create @@ -2163,8 +2230,10 @@ sub find_or_create { my $self = shift; my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); my $hash = ref $_[0] eq 'HASH' ? shift : {@_}; - my $exists = $self->find($hash, $attrs); - return defined $exists ? $exists : $self->create($hash); + if (keys %$hash and my $row = $self->find($hash, $attrs) ) { + return $row; + } + return $self->create($hash); } =head2 update_or_create @@ -2489,10 +2558,17 @@ sub _resolve_from { my $source = $self->result_source; my $attrs = $self->{attrs}; - my $from = $attrs->{from} - || [ { $attrs->{alias} => $source->from } ]; + my $from = [ @{ + $attrs->{from} + || + [{ + -source_handle => $source->handle, + -alias => $attrs->{alias}, + $attrs->{alias} => $source->from, + }] + }]; - my $seen = { %{$attrs->{seen_join}||{}} }; + my $seen = { %{$attrs->{seen_join} || {} } }; # we need to take the prefetch the attrs into account before we # ->_resolve_join as otherwise they get lost - captainL @@ -2509,6 +2585,12 @@ sub _resolve_from { return ($from,$seen); } +# too many times we have to do $attrs = { %{$self->_resolved_attrs} } +sub _resolved_attrs_copy { + my $self = shift; + return { %{$self->_resolved_attrs (@_)} }; +} + sub _resolved_attrs { my $self = shift; return $self->{_attrs} if $self->{_attrs}; @@ -2589,28 +2671,32 @@ sub _resolved_attrs { push( @{ $attrs->{as} }, @$adds ); } - $attrs->{from} ||= [ { $self->{attrs}{alias} => $source->from } ]; + $attrs->{from} ||= [ { + -source_handle => $source->handle, + -alias => $self->{attrs}{alias}, + $self->{attrs}{alias} => $source->from, + } ]; + + 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'; - if ( exists $attrs->{join} || exists $attrs->{prefetch} ) { 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->{group_by} ||= $attrs->{select} - if delete $attrs->{distinct}; if ( $attrs->{order_by} ) { $attrs->{order_by} = ( ref( $attrs->{order_by} ) eq 'ARRAY' @@ -2622,25 +2708,43 @@ sub _resolved_attrs { $attrs->{order_by} = []; } - my $collapse = $attrs->{collapse} || {}; + # 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 ]; + + + $attrs->{collapse} ||= {}; if ( my $prefetch = delete $attrs->{prefetch} ) { $prefetch = $self->_merge_attr( {}, $prefetch ); - my @pre_order; - foreach my $p ( ref $prefetch eq 'ARRAY' ? @$prefetch : ($prefetch) ) { - - # bring joins back to level of current class - my $join_map = $self->_joinpath_aliases ($attrs->{from}, $attrs->{seen_join}); - my @prefetch = - $source->_resolve_prefetch( $p, $alias, $join_map, \@pre_order, $collapse ); - push( @{ $attrs->{select} }, map { $_->[0] } @prefetch ); - push( @{ $attrs->{as} }, map { $_->[1] } @prefetch ); - } - push( @{ $attrs->{order_by} }, @pre_order ); + + my $prefetch_ordering = []; + + my $join_map = $self->_joinpath_aliases ($attrs->{from}, $attrs->{seen_join}); + + my @prefetch = + $source->_resolve_prefetch( $prefetch, $alias, $join_map, $prefetch_ordering, $attrs->{collapse} ); + + push( @{ $attrs->{select} }, map { $_->[0] } @prefetch ); + push( @{ $attrs->{as} }, map { $_->[1] } @prefetch ); + + push( @{ $attrs->{order_by} }, @$prefetch_ordering ); + $attrs->{_collapse_order_by} = \@$prefetch_ordering; } - $attrs->{collapse} = $collapse; - if ( $attrs->{page} and not defined $attrs->{offset} ) { - $attrs->{offset} = ( $attrs->{rows} * ( $attrs->{page} - 1 ) ); + + if (delete $attrs->{distinct}) { + $attrs->{group_by} ||= [ grep { !ref($_) || (ref($_) ne 'HASH') } @{$attrs->{select}} ]; + } + + # 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; @@ -2659,7 +2763,7 @@ sub _joinpath_aliases { my $p = $paths; $p = $p->{$_} ||= {} for @{$j->[0]{-join_path}}; - push @{$p->{-join_aliases} }, $j->[0]{-join_alias}; + push @{$p->{-join_aliases} }, $j->[0]{-alias}; } return $paths; @@ -3312,9 +3416,21 @@ with a father in the person table, we could explicitly use C: # SELECT child.* FROM person child # INNER JOIN person father ON child.father_id = father.id -If you need to express really complex joins or you need a subselect, you +You can select from a subquery by passing a resultset to from as follows. + + $schema->resultset('Artist')->search( + undef, + { alias => 'artist2', + from => [ { artist2 => $artist_rs->as_query } ], + } ); + + # and you'll get sql like this.. + # SELECT artist2.artistid, artist2.name, artist2.rank, artist2.charfield FROM + # ( SELECT me.artistid, me.name, me.rank, me.charfield FROM artists me ) artist2 + +If you need to express really complex joins, you can supply literal SQL to C via a scalar reference. In this case -the contents of the scalar will replace the table name asscoiated with the +the contents of the scalar will replace the table name associated with the resultsource. WARNING: This technique might very well not work as expected on chained