X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FResultSet.pm;h=9b5a42b7fe830ee813f9acb2126f2683b373c4b0;hb=c295dc9ed4703d80c2ecfdad613e6006249a0c72;hp=aa76a6d36683e9e1048890c8ada2304a9755e257;hpb=6a588797377dd1e3b80b54c6e6d9b577512587cb;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/ResultSet.pm b/lib/DBIx/Class/ResultSet.pm index aa76a6d..9b5a42b 100644 --- a/lib/DBIx/Class/ResultSet.pm +++ b/lib/DBIx/Class/ResultSet.pm @@ -6,8 +6,10 @@ use overload '0+' => \&count, 'bool' => sub { 1; }, fallback => 1; +use Carp::Clan qw/^DBIx::Class/; use Data::Page; use Storable; +use Data::Dumper; use Scalar::Util qw/weaken/; use DBIx::Class::ResultSetColumn; @@ -136,11 +138,51 @@ call it as C. sub search { my $self = shift; - - my $attrs = { %{$self->{attrs}} }; - my $having = delete $attrs->{having}; - $attrs = { %$attrs, %{ pop(@_) } } if @_ > 1 and ref $_[$#_] eq 'HASH'; + my $rs = $self->search_rs( @_ ); + return (wantarray ? $rs->all : $rs); +} + +=head2 search_rs + +=over 4 + +=item Arguments: $cond, \%attrs? +=item Return Value: $resultset + +=back + +This method does the same exact thing as search() except it will +always return a resultset, even in list context. + +=cut + +sub search_rs { + my $self = shift; + + my $our_attrs = { %{$self->{attrs}} }; + my $having = delete $our_attrs->{having}; + my $attrs = {}; + $attrs = pop(@_) if @_ > 1 and ref $_[$#_] eq 'HASH'; + + # merge new attrs into old + foreach my $key (qw/join prefetch/) { + next unless (exists $attrs->{$key}); + if (exists $our_attrs->{$key}) { + $our_attrs->{$key} = $self->_merge_attr($our_attrs->{$key}, $attrs->{$key}); + } else { + $our_attrs->{$key} = $attrs->{$key}; + } + delete $attrs->{$key}; + } + + if (exists $our_attrs->{prefetch}) { + $our_attrs->{join} = $self->_merge_attr($our_attrs->{join}, $our_attrs->{prefetch}, 1); + } + + my $new_attrs = { %{$our_attrs}, %{$attrs} }; + + # merge new where and having into old my $where = (@_ ? ((@_ == 1 || ref $_[0] eq "HASH") ? shift @@ -150,32 +192,32 @@ sub search { : {@_})) : undef()); if (defined $where) { - $attrs->{where} = (defined $attrs->{where} + $new_attrs->{where} = (defined $new_attrs->{where} ? { '-and' => [ map { ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ } - $where, $attrs->{where} ] } + $where, $new_attrs->{where} ] } : $where); } if (defined $having) { - $attrs->{having} = (defined $attrs->{having} + $new_attrs->{having} = (defined $new_attrs->{having} ? { '-and' => [ map { ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ } - $having, $attrs->{having} ] } + $having, $new_attrs->{having} ] } : $having); } - my $rs = (ref $self)->new($self->result_source, $attrs); + my $rs = (ref $self)->new($self->result_source, $new_attrs); $rs->{_parent_rs} = $self->{_parent_rs} if ($self->{_parent_rs}); #XXX - hack to pass through parent of related resultsets unless (@_) { # no search, effectively just a clone my $rows = $self->get_cache; - if( @{$rows} ) { + if ($rows) { $rs->set_cache($rows); } } - return (wantarray ? $rs->all : $rs); + return $rs; } =head2 search_literal @@ -196,7 +238,6 @@ resultset query. =cut -# TODO: needs fixing sub search_literal { my ($self, $cond, @vals) = @_; my $attrs = (ref $vals[$#vals] eq 'HASH' ? { %{ pop(@vals) } } : {}); @@ -234,9 +275,6 @@ Additionally, you can specify the columns explicitly by name: { key => 'artist_title' } ); -If no C is specified and you explicitly name columns, it searches on all -unique constraints defined on the source, including the primary key. - If the C is specified as C, it searches only on the primary key. See also L and L. For information on how to @@ -249,58 +287,43 @@ sub find { my $self = shift; my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); - # Parse out a hash from input + # Default to the primary key, but allow a specific key my @cols = exists $attrs->{key} ? $self->result_source->unique_constraint_columns($attrs->{key}) : $self->result_source->primary_columns; + $self->throw_exception( + "Can't find unless a primary key or unique constraint is defined" + ) unless @cols; - my $hash; + # Parse out a hashref from input + my $query; if (ref $_[0] eq 'HASH') { - $hash = { %{$_[0]} }; + $query = { %{$_[0]} }; } elsif (@_ == @cols) { - $hash = {}; - @{$hash}{@cols} = @_; + $query = {}; + @{$query}{@cols} = @_; } else { - $self->throw_exception( - "Arguments to find must be a hashref or match the number of columns in the " - . exists $attrs->{key} ? "$attrs->{key} unique constraint" : "primary key" - ); + # Compatibility: Allow e.g. find(id => $value) + carp "find by key => value deprecated; please use a hashref instead"; + $query = {@_}; } - # Check the hash we just parsed against our source's unique constraints - my @constraint_names = exists $attrs->{key} - ? ($attrs->{key}) - : $self->result_source->unique_constraint_names; - $self->throw_exception( - "Can't find unless a primary key or unique constraint is defined" - ) unless @constraint_names; - - my @unique_queries; - foreach my $name (@constraint_names) { - my @unique_cols = $self->result_source->unique_constraint_columns($name); - my $unique_query = $self->_build_unique_query($hash, \@unique_cols); - - # Add the ResultSet's alias - foreach my $key (grep { ! m/\./ } keys %$unique_query) { - my $alias = $self->{attrs}->{alias}; - $unique_query->{"$alias.$key"} = delete $unique_query->{$key}; - } - - push @unique_queries, $unique_query if %$unique_query; + # Add the ResultSet's alias + foreach my $key (grep { ! m/\./ } keys %$query) { + $query->{"$self->{attrs}{alias}.$key"} = delete $query->{$key}; } - # Handle cases where the ResultSet already defines the query - my $query = @unique_queries ? \@unique_queries : undef; - # Run the query if (keys %$attrs) { my $rs = $self->search($query, $attrs); - return $rs->{attrs}->{prefetch} ? $rs->next : $rs->single; + $rs->_resolve; + return keys %{$rs->{_attrs}->{collapse}} ? $rs->next : $rs->single; } else { - return ($self->{attrs}->{prefetch}) + $self->_resolve; + return (keys %{$self->{_attrs}->{collapse}}) ? $self->search($query)->next : $self->single($query); } @@ -384,6 +407,10 @@ sub cursor { Inflates the first result without creating a cursor if the resultset has any records in it; if not returns nothing. Used by L as an optimisation. +Can optionally take an additional condition *only* - this is a fast-code-path +method; if you need to add extra joins or similar call ->search and then +->single without a condition on the $rs returned from that. + =cut sub single { @@ -516,9 +543,9 @@ first record from the resultset. sub next { my ($self) = @_; - if (@{$self->{all_cache} || []}) { + if (my $cache = $self->get_cache) { $self->{all_cache_position} ||= 0; - return $self->{all_cache}->[$self->{all_cache_position}++]; + return $cache->[$self->{all_cache_position}++]; } if ($self->{attrs}{cache}) { $self->{all_cache_position} = 1; @@ -532,7 +559,6 @@ sub next { return $self->_construct_object(@row); } -# XXX - this is essentially just the old new(). rewrite / tidy up? sub _resolve { my $self = shift; @@ -541,9 +567,12 @@ sub _resolve { my $attrs = $self->{attrs}; my $source = ($self->{_parent_rs}) ? $self->{_parent_rs} : $self->{result_source}; - # XXX - this is a hack to prevent dclone dieing because of the code ref, get's put back in $attrs afterwards + # XXX - lose storable dclone my $record_filter = delete $attrs->{record_filter} if (defined $attrs->{record_filter}); $attrs = Storable::dclone($attrs || {}); # { %{ $attrs || {} } }; + $attrs->{record_filter} = $record_filter if ($record_filter); + $self->{attrs}->{record_filter} = $record_filter if ($record_filter); + my $alias = $attrs->{alias}; $attrs->{columns} ||= delete $attrs->{cols} if $attrs->{cols}; @@ -560,7 +589,6 @@ sub _resolve { push(@{$attrs->{select}}, @$include); push(@{$attrs->{as}}, map { m/([^.]+)$/; $1; } @$include); } - #use Data::Dumper; warn Dumper(@{$attrs}{qw/select as/}); $attrs->{from} ||= [ { $alias => $source->from } ]; $attrs->{seen_join} ||= {}; @@ -576,11 +604,22 @@ sub _resolve { push(@{$attrs->{from}}, $source->resolve_join($join, $attrs->{alias}, $attrs->{seen_join})); } - $attrs->{group_by} ||= $attrs->{select} if delete $attrs->{distinct}; $attrs->{order_by} = [ $attrs->{order_by} ] if $attrs->{order_by} and !ref($attrs->{order_by}); $attrs->{order_by} ||= []; + + if(my $seladds = delete($attrs->{'+select'})) { + my @seladds = (ref($seladds) eq 'ARRAY' ? @$seladds : ($seladds)); + $attrs->{select} = [ + @{ $attrs->{select} }, + map { (m/\./ || ref($_)) ? $_ : "${alias}.$_" } $seladds + ]; + } + if(my $asadds = delete($attrs->{'+as'})) { + my @asadds = (ref($asadds) eq 'ARRAY' ? @$asadds : ($asadds)); + $attrs->{as} = [ @{ $attrs->{as} }, @asadds ]; + } my $collapse = $attrs->{collapse} || {}; if (my $prefetch = delete $attrs->{prefetch}) { @@ -603,16 +642,68 @@ sub _resolve { push(@{$attrs->{order_by}}, @pre_order); } $attrs->{collapse} = $collapse; - $attrs->{record_filter} = $record_filter if ($record_filter); $self->{_attrs} = $attrs; } +sub _merge_attr { + my ($self, $a, $b, $is_prefetch) = @_; + + return $b unless $a; + if (ref $b eq 'HASH' && ref $a eq 'HASH') { + foreach my $key (keys %{$b}) { + if (exists $a->{$key}) { + $a->{$key} = $self->_merge_attr($a->{$key}, $b->{$key}, $is_prefetch); + } else { + $a->{$key} = delete $b->{$key}; + } + } + return $a; + } else { + $a = [$a] unless (ref $a eq 'ARRAY'); + $b = [$b] unless (ref $b eq 'ARRAY'); + + my $hash = {}; + my $array = []; + foreach ($a, $b) { + foreach my $element (@{$_}) { + if (ref $element eq 'HASH') { + $hash = $self->_merge_attr($hash, $element, $is_prefetch); + } elsif (ref $element eq 'ARRAY') { + $array = [@{$array}, @{$element}]; + } else { + if (($b == $_) && $is_prefetch) { + $self->_merge_array($array, $element, $is_prefetch); + } else { + push(@{$array}, $element); + } + } + } + } + + if ((keys %{$hash}) && (scalar(@{$array} > 0))) { + return [$hash, @{$array}]; + } else { + return (keys %{$hash}) ? $hash : $array; + } + } +} + +sub _merge_array { + my ($self, $a, $b) = @_; + + $b = [$b] unless (ref $b eq 'ARRAY'); + # add elements from @{$b} to @{$a} which aren't already in @{$a} + foreach my $b_element (@{$b}) { + push(@{$a}, $b_element) unless grep {$b_element eq $_} @{$a}; + } +} + sub _construct_object { my ($self, @row) = @_; my @as = @{ $self->{_attrs}{as} }; my $info = $self->_collapse_result(\@as, \@row); - my $new = $self->result_class->inflate_result($self->result_source, @$info, $self->{_parent_rs}); + 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; @@ -682,7 +773,8 @@ sub _collapse_result { $row = $self->{stashed_row} = \@raw; $tree = $self->_collapse_result($as, $row, $c_prefix); } - @$target = @final; + @$target = (@final ? @final : [ {}, {} ]); + # single empty result to indicate an empty prefetched has_many } return $info; } @@ -728,7 +820,7 @@ clause. sub count { my $self = shift; return $self->search(@_)->count if @_ and defined $_[0]; - return scalar @{ $self->get_cache } if @{ $self->get_cache }; + return scalar @{ $self->get_cache } if $self->get_cache; my $count = $self->_count; return 0 unless $count; @@ -760,7 +852,6 @@ sub _count { # Separated out so pager can get the full count } $select = { count => { distinct => \@distinct } }; - #use Data::Dumper; die Dumper $select; } $attrs->{select} = $select; @@ -768,7 +859,6 @@ sub _count { # Separated out so pager can get the full count # offset, order by and page are not needed to count. record_filter is cdbi delete $attrs->{$_} for qw/rows offset order_by page pager record_filter/; - my ($count) = (ref $self)->new($self->result_source, $attrs)->cursor->next; return $count; } @@ -807,14 +897,14 @@ is returned in list context. sub all { my ($self) = @_; - return @{ $self->get_cache } if @{ $self->get_cache }; + return @{ $self->get_cache } if $self->get_cache; my @obj; - # XXX used to be 'if (keys %{$self->{collapse}})' - # XXX replaced by this as it seemed to do roughly the same thing - # XXX could be bad as never really understood exactly what collapse did - if ($self->{attrs}->{prefetch}) { + # TODO: don't call resolve here + $self->_resolve; + if (keys %{$self->{_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 @@ -906,7 +996,7 @@ sub _cond_for_update_delete { $cond->{-and} = []; my @cond = @{$self->{cond}{-and}}; - for (my $i = 0; $i < @cond - 1; $i++) { + for (my $i = 0; $i <= @cond - 1; $i++) { my $entry = $cond[$i]; my %hash; @@ -918,7 +1008,7 @@ sub _cond_for_update_delete { } else { $entry =~ /([^.]+)$/; - $hash{$entry} = $cond[++$i]; + $hash{$1} = $cond[++$i]; } push @{$cond->{-and}}, \%hash; @@ -1260,11 +1350,29 @@ sub update_or_create { my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); my $hash = ref $_[0] eq 'HASH' ? shift : {@_}; - my $row = $self->find($hash, $attrs); - if (defined $row) { - $row->set_columns($hash); - $row->update; - return $row; + my @constraint_names = exists $attrs->{key} + ? ($attrs->{key}) + : $self->result_source->unique_constraint_names; + $self->throw_exception( + "update_or_create requires a primary key or unique constraint; none is defined on " + . $self->result_source->name + ) unless @constraint_names; + + my @unique_queries; + foreach my $name (@constraint_names) { + my @unique_cols = $self->result_source->unique_constraint_columns($name); + my $unique_query = $self->_build_unique_query($hash, \@unique_cols); + + push @unique_queries, $unique_query + if keys %$unique_query == @unique_cols; + } + + if (@unique_queries) { + my $row = $self->single(\@unique_queries); + if (defined $row) { + $row->update($hash); + return $row; + } } return $self->create($hash); @@ -1285,7 +1393,7 @@ Gets the contents of the cache for the resultset, if the cache is set. =cut sub get_cache { - shift->{all_cache} || []; + shift->{all_cache}; } =head2 set_cache @@ -1308,13 +1416,7 @@ than re-querying the database even if the cache attr is not set. sub set_cache { my ( $self, $data ) = @_; $self->throw_exception("set_cache requires an arrayref") - if ref $data ne 'ARRAY'; - my $result_class = $self->result_class; - foreach( @$data ) { - $self->throw_exception( - "cannot cache object of type '$_', expected '$result_class'" - ) if ref $_ ne $result_class; - } + if defined($data) && (ref $data ne 'ARRAY'); $self->{all_cache} = $data; } @@ -1333,7 +1435,7 @@ Clears the cache for the resultset. =cut sub clear_cache { - shift->set_cache([]); + shift->set_cache(undef); } =head2 related_resultset @@ -1409,6 +1511,11 @@ Which column(s) to order the results by. This is currently passed through directly to SQL, so you can give e.g. C for a descending order on the column `year'. +Please note that if you have quoting enabled (see +L) you will need to do C<\'year DESC' > to +specify an order. (The scalar ref causes it to be passed as raw sql to the DB, +so you will need to manually quote things as appropriate.) + =head2 columns =over 4 @@ -1464,6 +1571,23 @@ When you use function/stored procedure names and do not supply an C attribute, the column names returned are storage-dependent. E.g. MySQL would return a column named C in the above example. +=head2 +select + +=over 4 + +Indicates additional columns to be selected from storage. Works the same as +L