X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FResultSet.pm;h=56a55d4705ea8a3c6096eb50efdb73400f270d18;hb=9387c9042568a6c4dcc8e197f0000e7c7c4bbf13;hp=186a39c04260b8a88a5afae1a2f5529606855715;hpb=ff7bb7a13a5254d5af455d17eeaa8b629d936183;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/ResultSet.pm b/lib/DBIx/Class/ResultSet.pm index 186a39c..56a55d4 100644 --- a/lib/DBIx/Class/ResultSet.pm +++ b/lib/DBIx/Class/ResultSet.pm @@ -3,15 +3,17 @@ package DBIx::Class::ResultSet; use strict; use warnings; use overload - '0+' => 'count', + '0+' => \&count, 'bool' => sub { 1; }, fallback => 1; use Data::Page; use Storable; +use Scalar::Util qw/weaken/; +use DBIx::Class::ResultSetColumn; use base qw/DBIx::Class/; __PACKAGE__->load_components(qw/AccessorGroup/); -__PACKAGE__->mk_group_accessors('simple' => 'result_source'); +__PACKAGE__->mk_group_accessors('simple' => qw/result_source result_class/); =head1 NAME @@ -20,7 +22,7 @@ DBIx::Class::ResultSet - Responsible for fetching and creating resultset. =head1 SYNOPSIS my $rs = $schema->resultset('User')->search(registered => 1); - my @rows = $schema->resultset('Foo')->search(bar => 'baz'); + my @rows = $schema->resultset('CD')->search(year => 2005); =head1 DESCRIPTION @@ -32,6 +34,7 @@ In the examples below, the following table classes are used: package MyApp::Schema::Artist; use base qw/DBIx::Class/; + __PACKAGE__->load_components(qw/Core/); __PACKAGE__->table('artist'); __PACKAGE__->add_columns(qw/artistid name/); __PACKAGE__->set_primary_key('artistid'); @@ -40,7 +43,8 @@ In the examples below, the following table classes are used: package MyApp::Schema::CD; use base qw/DBIx::Class/; - __PACKAGE__->table('artist'); + __PACKAGE__->load_components(qw/Core/); + __PACKAGE__->table('cd'); __PACKAGE__->add_columns(qw/cdid artist title year/); __PACKAGE__->set_primary_key('cdid'); __PACKAGE__->belongs_to(artist => 'MyApp::Schema::Artist'); @@ -48,147 +52,207 @@ In the examples below, the following table classes are used: =head1 METHODS -=head2 new($source, \%$attrs) +=head2 new + +=over 4 + +=item Arguments: $source, \%$attrs + +=item Return Value: $rs + +=back The resultset constructor. Takes a source object (usually a -L) and an attribute hash (see L -below). Does not perform any queries -- these are executed as needed by the -other methods. +L) and an attribute hash (see +L below). Does not perform any queries -- these are +executed as needed by the other methods. Generally you won't need to construct a resultset manually. You'll automatically get one from e.g. a L called in scalar context: my $rs = $schema->resultset('CD')->search({ title => '100th Window' }); +IMPORTANT: If called on an object, proxies to new_result instead so + + my $cd = $schema->resultset('CD')->new({ title => 'Spoon' }); + +will return a CD object, not a ResultSet. + =cut sub new { my $class = shift; return $class->new_result(@_) if ref $class; + my ($source, $attrs) = @_; - #use Data::Dumper; warn Dumper($attrs); + weaken $source; $attrs = Storable::dclone($attrs || {}); # { %{ $attrs || {} } }; - my %seen; + #use Data::Dumper; warn Dumper($attrs); my $alias = ($attrs->{alias} ||= 'me'); - if ($attrs->{cols} || !$attrs->{select}) { - delete $attrs->{as} if $attrs->{cols}; - my @cols = ($attrs->{cols} - ? @{delete $attrs->{cols}} - : $source->columns); - $attrs->{select} = [ map { m/\./ ? $_ : "${alias}.$_" } @cols ]; - } - $attrs->{as} ||= [ map { m/^$alias\.(.*)$/ ? $1 : $_ } @{$attrs->{select}} ]; + + $attrs->{columns} ||= delete $attrs->{cols} if $attrs->{cols}; + delete $attrs->{as} if $attrs->{columns}; + $attrs->{columns} ||= [ $source->columns ] unless $attrs->{select}; + $attrs->{select} = [ + map { m/\./ ? $_ : "${alias}.$_" } @{delete $attrs->{columns}} + ] if $attrs->{columns}; + $attrs->{as} ||= [ + map { m/^\Q$alias.\E(.+)$/ ? $1 : $_ } @{$attrs->{select}} + ]; if (my $include = delete $attrs->{include_columns}) { push(@{$attrs->{select}}, @$include); - push(@{$attrs->{as}}, map { m/([^\.]+)$/; $1; } @$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} ||= {}; + my %seen; if (my $join = delete $attrs->{join}) { - foreach my $j (ref $join eq 'ARRAY' - ? (@{$join}) : ($join)) { + foreach my $j (ref $join eq 'ARRAY' ? @$join : ($join)) { if (ref $j eq 'HASH') { $seen{$_} = 1 foreach keys %$j; } else { $seen{$j} = 1; } } - push(@{$attrs->{from}}, $source->resolve_join($join, $attrs->{alias}, $attrs->{seen_join})); + 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} ||= []; + my $collapse = $attrs->{collapse} || {}; if (my $prefetch = delete $attrs->{prefetch}) { - foreach my $p (ref $prefetch eq 'ARRAY' - ? (@{$prefetch}) : ($prefetch)) { - if( ref $p eq 'HASH' ) { + my @pre_order; + foreach my $p (ref $prefetch eq 'ARRAY' ? @$prefetch : ($prefetch)) { + if ( ref $p eq 'HASH' ) { foreach my $key (keys %$p) { push(@{$attrs->{from}}, $source->resolve_join($p, $attrs->{alias})) unless $seen{$key}; } - } - else { + } else { push(@{$attrs->{from}}, $source->resolve_join($p, $attrs->{alias})) unless $seen{$p}; } - my @prefetch = $source->resolve_prefetch($p, $attrs->{alias}); - #die Dumper \@cols; + my @prefetch = $source->resolve_prefetch( + $p, $attrs->{alias}, {}, \@pre_order, $collapse); push(@{$attrs->{select}}, map { $_->[0] } @prefetch); push(@{$attrs->{as}}, map { $_->[1] } @prefetch); } + push(@{$attrs->{order_by}}, @pre_order); } + $attrs->{collapse} = $collapse; +# use Data::Dumper; warn Dumper($collapse) if keys %{$collapse}; if ($attrs->{page}) { $attrs->{rows} ||= 10; $attrs->{offset} ||= 0; $attrs->{offset} += ($attrs->{rows} * ($attrs->{page} - 1)); } - my $new = { + + bless { result_source => $source, + result_class => $attrs->{result_class} || $source->result_class, cond => $attrs->{where}, from => $attrs->{from}, + collapse => $collapse, count => undef, page => delete $attrs->{page}, pager => undef, - attrs => $attrs }; - bless ($new, $class); - return $new; + attrs => $attrs + }, $class; } =head2 search - my @obj = $rs->search({ foo => 3 }); # "... WHERE foo = 3" - my $new_rs = $rs->search({ foo => 3 }); +=over 4 + +=item Arguments: $cond, \%attrs? + +=item Return Value: $resultset (scalar context), @row_objs (list context) + +=back + + my @cds = $cd_rs->search({ year => 2001 }); # "... WHERE year = 2001" + my $new_rs = $cd_rs->search({ year => 2005 }); + + my $new_rs = $cd_rs->search([ { year => 2005 }, { year => 2004 } ]); + # year = 2005 OR year = 2004 If you need to pass in additional attributes but no additional condition, -call it as C. +call it as C. - # "SELECT foo, bar FROM $class_table" - my @all = $class->search({}, { cols => [qw/foo bar/] }); + # "SELECT name, artistid FROM $artist_table" + my @all_artists = $schema->resultset('Artist')->search(undef, { + columns => [qw/name artistid/], + }); =cut sub search { my $self = shift; - - #use Data::Dumper;warn Dumper(@_); - my $rs; - if( @_ ) { - my $attrs = { %{$self->{attrs}} }; - if (@_ > 1 && ref $_[$#_] eq 'HASH') { - $attrs = { %$attrs, %{ pop(@_) } }; - } - - my $where = (@_ ? ((@_ == 1 || ref $_[0] eq "HASH") ? shift : {@_}) : undef()); - if (defined $where) { - $where = (defined $attrs->{where} - ? { '-and' => - [ map { ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ } - $where, $attrs->{where} ] } - : $where); - $attrs->{where} = $where; - } + my $attrs = { %{$self->{attrs}} }; + my $having = delete $attrs->{having}; + $attrs = { %$attrs, %{ pop(@_) } } if @_ > 1 and ref $_[$#_] eq 'HASH'; + + my $where = (@_ + ? ((@_ == 1 || ref $_[0] eq "HASH") + ? shift + : ((@_ % 2) + ? $self->throw_exception( + "Odd number of arguments to search") + : {@_})) + : undef()); + if (defined $where) { + $attrs->{where} = (defined $attrs->{where} + ? { '-and' => + [ map { ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ } + $where, $attrs->{where} ] } + : $where); + } - $rs = (ref $self)->new($self->result_source, $attrs); + if (defined $having) { + $attrs->{having} = (defined $attrs->{having} + ? { '-and' => + [ map { ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ } + $having, $attrs->{having} ] } + : $having); } - else { - $rs = $self; - $rs->reset(); + + my $rs = (ref $self)->new($self->result_source, $attrs); + + my $rows = $self->get_cache; + if( @{$rows} ) { + $rs->set_cache($rows); } + return (wantarray ? $rs->all : $rs); } =head2 search_literal - my @obj = $rs->search_literal($literal_where_cond, @bind); - my $new_rs = $rs->search_literal($literal_where_cond, @bind); +=over 4 + +=item Arguments: $sql_fragment, @bind_values + +=item Return Value: $resultset (scalar context), @row_objs (list context) + +=back + + my @cds = $cd_rs->search_literal('year = ? AND title = ?', qw/2001 Reload/); + my $newrs = $artist_rs->search_literal('name = ?', 'Metallica'); Pass a literal chunk of SQL to be added to the conditional part of the -resultset. +resultset query. =cut - + sub search_literal { my ($self, $cond, @vals) = @_; my $attrs = (ref $vals[$#vals] eq 'HASH' ? { %{ pop(@vals) } } : {}); @@ -196,16 +260,29 @@ sub search_literal { return $self->search(\$cond, $attrs); } -=head2 find(@colvalues), find(\%cols, \%attrs?) +=head2 find + +=over 4 + +=item Arguments: @values | \%cols, \%attrs? -Finds a row based on its primary key or unique constraint. For example: +=item Return Value: $row_object + +=back + +Finds a row based on its primary key or unique constraint. For example, to find +a row by its primary key: my $cd = $schema->resultset('CD')->find(5); -Also takes an optional C attribute, to search by a specific key or unique -constraint. For example: +You can also find a row by a specific unique constraint using the C +attribute. For example: - my $cd = $schema->resultset('CD')->find_or_create( + my $cd = $schema->resultset('CD')->find('Massive Attack', 'Mezzanine', { key => 'artist_title' }); + +Additionally, you can specify the columns explicitly by name: + + my $cd = $schema->resultset('CD')->find( { artist => 'Massive Attack', title => 'Mezzanine', @@ -213,50 +290,108 @@ constraint. For example: { key => 'artist_title' } ); -See also L and L. +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 +declare unique constraints, see +L. =cut sub find { - my ($self, @vals) = @_; - my $attrs = (@vals > 1 && ref $vals[$#vals] eq 'HASH' ? pop(@vals) : {}); - - my @cols = $self->result_source->primary_columns; - if (exists $attrs->{key}) { - my %uniq = $self->result_source->unique_constraints; - $self->( "Unknown key " . $attrs->{key} . " on " . $self->name ) - unless exists $uniq{$attrs->{key}}; - @cols = @{ $uniq{$attrs->{key}} }; + my $self = shift; + my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); + + # Parse out a hash from input + my @cols = exists $attrs->{key} + ? $self->result_source->unique_constraint_columns($attrs->{key}) + : $self->result_source->primary_columns; + + my $hash; + if (ref $_[0] eq 'HASH') { + $hash = { %{$_[0]} }; } - #use Data::Dumper; warn Dumper($attrs, @vals, @cols); - $self->throw_exception( "Can't find unless a primary key or unique constraint is defined" ) - unless @cols; - - my $query; - if (ref $vals[0] eq 'HASH') { - $query = { %{$vals[0]} }; - } elsif (@cols == @vals) { - $query = {}; - @{$query}{@cols} = @vals; - } else { - $query = {@vals}; + elsif (@_ == @cols) { + $hash = {}; + @{$hash}{@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" + ); } - foreach (keys %$query) { - next if m/\./; - $query->{$self->{attrs}{alias}.'.'.$_} = delete $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) { + $unique_query->{"$self->{attrs}{alias}.$key"} = delete $unique_query->{$key}; + } + + push @unique_queries, $unique_query if %$unique_query; + } + + # 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 keys %{$rs->{collapse}} ? $rs->next : $rs->single; + } + else { + return keys %{$self->{collapse}} + ? $self->search($query)->next + : $self->single($query); } - #warn Dumper($query); - return (keys %$attrs - ? $self->search($query,$attrs)->single - : $self->single($query)); +} + +# _build_unique_query +# +# Constrain the specified query hash based on the specified column names. + +sub _build_unique_query { + my ($self, $query, $unique_cols) = @_; + + my %unique_query = + map { $_ => $query->{$_} } + grep { exists $query->{$_} } + @$unique_cols; + + return \%unique_query; } =head2 search_related - $rs->search_related('relname', $cond?, $attrs?); +=over 4 + +=item Arguments: $cond, \%attrs? + +=item Return Value: $new_resultset -Search the specified relationship. Optionally specify a condition for matching -records. +=back + + $new_rs = $cd_rs->search_related('artist', { + name => 'Emo-R-Us', + }); + +Searches the specified relationship, optionally specifying a condition and +attributes for matching records. See L for more information. =cut @@ -266,14 +401,22 @@ sub search_related { =head2 cursor -Returns a storage-driven cursor to the given resultset. +=over 4 + +=item Arguments: none + +=item Return Value: $cursor + +=back + +Returns a storage-driven cursor to the given resultset. See +L for more information. =cut sub cursor { my ($self) = @_; - my ($attrs) = $self->{attrs}; - $attrs = { %$attrs }; + my $attrs = { %{$self->{attrs}} }; return $self->{cursor} ||= $self->result_source->storage->select($self->{from}, $attrs->{select}, $attrs->{where},$attrs); @@ -281,23 +424,33 @@ sub cursor { =head2 single -Inflates the first result without creating a cursor +=over 4 + +=item Arguments: $cond? + +=item Return Value: $row_object? + +=back + + my $cd = $schema->resultset('CD')->single({ year => 2001 }); + +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. =cut sub single { - my ($self, $extra) = @_; - my ($attrs) = $self->{attrs}; - $attrs = { %$attrs }; - if ($extra) { + my ($self, $where) = @_; + my $attrs = { %{$self->{attrs}} }; + if ($where) { if (defined $attrs->{where}) { $attrs->{where} = { - '-and' - => [ map { ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ } - delete $attrs->{where}, $extra ] + '-and' => + [ map { ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ } + $where, delete $attrs->{where} ] }; } else { - $attrs->{where} = $extra; + $attrs->{where} = $where; } } my @data = $self->result_source->storage->select_single( @@ -306,11 +459,44 @@ sub single { return (@data ? $self->_construct_object(@data) : ()); } +=head2 get_column + +=over 4 + +=item Arguments: $cond? + +=item Return Value: $resultsetcolumn + +=back + + my $max_length = $rs->get_column('length')->max; + +Returns a ResultSetColumn instance for $column based on $self + +=cut + +sub get_column { + my ($self, $column) = @_; + + my $new = DBIx::Class::ResultSetColumn->new($self, $column); + return $new; +} =head2 search_like -Perform a search, but use C instead of equality as the condition. Note -that this is simply a convenience method; you most likely want to use +=over 4 + +=item Arguments: $cond, \%attrs? + +=item Return Value: $resultset (scalar context), @row_objs (list context) + +=back + + # WHERE title LIKE '%blue%' + $cd_rs = $rs->search_like({ title => '%blue%'}); + +Performs a search, but uses C instead of C<=> as the condition. Note +that this is simply a convenience method. You most likely want to use L with specific operators. For more information, see L. @@ -318,55 +504,81 @@ For more information, see L. =cut sub search_like { - my $class = shift; - my $attrs = { }; - if (@_ > 1 && ref $_[$#_] eq 'HASH') { - $attrs = pop(@_); - } - my $query = ref $_[0] eq "HASH" ? { %{shift()} }: {@_}; + my $class = shift; + my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); + my $query = ref $_[0] eq 'HASH' ? { %{shift()} }: {@_}; $query->{$_} = { 'like' => $query->{$_} } for keys %$query; return $class->search($query, { %$attrs }); } -=head2 slice($first, $last) +=head2 slice -Returns a subset of elements from the resultset. +=over 4 + +=item Arguments: $first, $last + +=item Return Value: $resultset (scalar context), @row_objs (list context) + +=back + +Returns a resultset or object list representing a subset of elements from the +resultset slice is called on. Indexes are from 0, i.e., to get the first +three records, call: + + my ($one, $two, $three) = $rs->slice(0, 2); =cut sub slice { my ($self, $min, $max) = @_; - my $attrs = { %{ $self->{attrs} || {} } }; - $attrs->{offset} ||= 0; + my $attrs = {}; # = { %{ $self->{attrs} || {} } }; + $attrs->{offset} = $self->{attrs}{offset} || 0; $attrs->{offset} += $min; $attrs->{rows} = ($max ? ($max - $min + 1) : 1); - my $slice = (ref $self)->new($self->result_source, $attrs); - return (wantarray ? $slice->all : $slice); + return $self->search(undef(), $attrs); + #my $slice = (ref $self)->new($self->result_source, $attrs); + #return (wantarray ? $slice->all : $slice); } =head2 next +=over 4 + +=item Arguments: none + +=item Return Value: $result? + +=back + Returns the next element in the resultset (C is there is none). Can be used to efficiently iterate over records in the resultset: - my $rs = $schema->resultset('CD')->search({}); + my $rs = $schema->resultset('CD')->search; while (my $cd = $rs->next) { print $cd->title; } +Note that you need to store the resultset object, and call C on it. +Calling C<< resultset('Table')->next >> repeatedly will always return the +first record from the resultset. + =cut sub next { my ($self) = @_; - my $cache = $self->get_cache; - if( @$cache ) { + if (@{$self->{all_cache} || []}) { $self->{all_cache_position} ||= 0; - my $obj = $cache->[$self->{all_cache_position}]; - $self->{all_cache_position}++; - return $obj; + return $self->{all_cache}->[$self->{all_cache_position}++]; + } + if ($self->{attrs}{cache}) { + $self->{all_cache_position} = 1; + return ($self->all)[0]; } - my @row = $self->cursor->next; + my @row = (exists $self->{stashed_row} ? + @{delete $self->{stashed_row}} : + $self->cursor->next + ); # warn Dumper(\@row); use Data::Dumper; return unless (@row); return $self->_construct_object(@row); @@ -374,89 +586,113 @@ sub next { sub _construct_object { my ($self, @row) = @_; - my @row_orig = @row; # copy @row for key comparison later, because @row will change my @as = @{ $self->{attrs}{as} }; - #warn "@cols -> @row"; + + my $info = $self->_collapse_result(\@as, \@row); + + 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; +} + +sub _collapse_result { + my ($self, $as, $row, $prefix) = @_; + + my %const; + + my @copy = @$row; + foreach my $this_as (@$as) { + my $val = shift @copy; + if (defined $prefix) { + if ($this_as =~ m/^\Q${prefix}.\E(.+)$/) { + my $remain = $1; + $remain =~ /^(?:(.*)\.)?([^.]+)$/; + $const{$1||''}{$2} = $val; + } + } else { + $this_as =~ /^(?:(.*)\.)?([^.]+)$/; + $const{$1||''}{$2} = $val; + } + } + my $info = [ {}, {} ]; - foreach my $as (@as) { - my $rs = $self; + foreach my $key (keys %const) { + if (length $key) { + my $target = $info; + my @parts = split(/\./, $key); + foreach my $p (@parts) { + $target = $target->[1]->{$p} ||= []; + } + $target->[0] = $const{$key}; + } else { + $info->[0] = $const{$key}; + } + } + + my @collapse; + if (defined $prefix) { + @collapse = map { + m/^\Q${prefix}.\E(.+)$/ ? ($1) : () + } keys %{$self->{collapse}} + } else { + @collapse = keys %{$self->{collapse}}; + }; + + if (@collapse) { + my ($c) = sort { length $a <=> length $b } @collapse; my $target = $info; - my @parts = split(/\./, $as); - my $col = pop(@parts); - foreach my $p (@parts) { + foreach my $p (split(/\./, $c)) { $target = $target->[1]->{$p} ||= []; - - # if cache is enabled, fetch inflated objs for prefetch - if( $rs->{attrs}->{cache} ) { - my $rel_info = $rs->result_source->relationship_info($p); - my $cond = $rel_info->{cond}; - my $parent_rs = $rs; - $rs = $rs->related_resultset($p); - $rs->{attrs}->{cache} = 1; - my @objs = (); - - # populate related resultset's cache if empty - if( !@{ $rs->get_cache } ) { - $rs->all; - } - - # get ordinals for pk columns in $row, so values can be compared - my $map = {}; - keys %$cond; - my $re = qr/^\w+\./; - while( my( $rel_key, $pk ) = ( each %$cond ) ) { - $rel_key =~ s/$re//; - $pk =~ s/$re//; - $map->{$rel_key} = $pk; - } #die Dumper $map; - - keys %$map; - while( my( $rel_key, $pk ) = each( %$map ) ) { - my $i = 0; - foreach my $col ( $parent_rs->result_source->columns ) { - if( $col eq $pk ) { - $map->{$rel_key} = $i; - } - $i++; - } - } #die Dumper $map; - - $rs->reset(); # reset cursor/cache position - - # get matching objects for inflation - OBJ: while( my $rel_obj = $rs->next ) { - keys %$map; - KEYS: while( my( $rel_key, $ordinal ) = each %$map ) { - # use get_column to avoid auto inflation (want scalar value) - if( $rel_obj->get_column($rel_key) ne $row_orig[$ordinal] ) { - next OBJ; - } - push @objs, $rel_obj; - } - } - $target->[0] = \@objs; - } } - $target->[0]->{$col} = shift @row - if ref($target->[0]) ne 'ARRAY'; # arrayref is pre-inflated objects, do not overwrite + my $c_prefix = (defined($prefix) ? "${prefix}.${c}" : $c); + my @co_key = @{$self->{collapse}{$c_prefix}}; + my %co_check = map { ($_, $target->[0]->{$_}); } @co_key; + my $tree = $self->_collapse_result($as, $row, $c_prefix); + my (@final, @raw); + while ( !(grep { + !defined($tree->[0]->{$_}) || + $co_check{$_} ne $tree->[0]->{$_} + } @co_key) ) { + push(@final, $tree); + last unless (@raw = $self->cursor->next); + $row = $self->{stashed_row} = \@raw; + $tree = $self->_collapse_result($as, $row, $c_prefix); + #warn Data::Dumper::Dumper($tree, $row); + } + @$target = @final; } - #use Data::Dumper; warn Dumper(\@as, $info); - my $new = $self->result_source->result_class->inflate_result( - $self->result_source, @$info); - $new = $self->{attrs}{record_filter}->($new) - if exists $self->{attrs}{record_filter}; - return $new; + + return $info; } -=head2 result_source +=head2 result_source + +=over 4 + +=item Arguments: $result_source? -Returns a reference to the result source for this recordset. +=item Return Value: $result_source + +=back + +An accessor for the primary ResultSource object from which this ResultSet +is derived. =cut =head2 count +=over 4 + +=item Arguments: $cond, \%attrs?? + +=item Return Value: $count + +=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. @@ -471,52 +707,62 @@ clause. sub count { my $self = shift; - return $self->search(@_)->count if @_ && defined $_[0]; - unless (defined $self->{count}) { - return scalar @{ $self->get_cache } - if @{ $self->get_cache }; - my $group_by; - my $select = { 'count' => '*' }; - if( $group_by = delete $self->{attrs}{group_by} ) { - my @distinct = (ref $group_by ? @$group_by : ($group_by)); - # todo: try CONCAT for multi-column pk - my @pk = $self->result_source->primary_columns; - if( scalar(@pk) == 1 ) { - my $pk = shift(@pk); - my $alias = $self->{attrs}{alias}; - my $re = qr/^($alias\.)?$pk$/; - foreach my $column ( @distinct) { - if( $column =~ $re ) { - @distinct = ( $column ); - last; - } - } - } + return $self->search(@_)->count if @_ and defined $_[0]; + return scalar @{ $self->get_cache } if @{ $self->get_cache }; - $select = { count => { 'distinct' => \@distinct } }; - #use Data::Dumper; die Dumper $select; - } + my $count = $self->_count; + return 0 unless $count; - my $attrs = { %{ $self->{attrs} }, - select => $select, - as => [ '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/; - - ($self->{count}) = (ref $self)->new($self->result_source, $attrs)->cursor->next; - $self->{attrs}{group_by} = $group_by; - } - return 0 unless $self->{count}; - my $count = $self->{count}; $count -= $self->{attrs}{offset} if $self->{attrs}{offset}; $count = $self->{attrs}{rows} if - ($self->{attrs}{rows} && $self->{attrs}{rows} < $count); + $self->{attrs}{rows} and $self->{attrs}{rows} < $count; + return $count; +} + +sub _count { # Separated out so pager can get the full count + my $self = shift; + my $select = { count => '*' }; + my $attrs = { %{ $self->{attrs} } }; + if (my $group_by = delete $attrs->{group_by}) { + delete $attrs->{having}; + my @distinct = (ref $group_by ? @$group_by : ($group_by)); + # todo: try CONCAT for multi-column pk + my @pk = $self->result_source->primary_columns; + if (@pk == 1) { + foreach my $column (@distinct) { + if ($column =~ qr/^(?:\Q$attrs->{alias}.\E)?$pk[0]$/) { + @distinct = ($column); + last; + } + } + } + + $select = { count => { distinct => \@distinct } }; + #use Data::Dumper; die Dumper $select; + } + + $attrs->{select} = $select; + $attrs->{as} = [qw/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; } =head2 count_literal -Calls L with the passed arguments, then L. +=over 4 + +=item Arguments: $sql_fragment, @bind_values + +=item Return Value: $count + +=back + +Counts the results in a literal query. Equivalent to calling L +with the passed arguments, then L. =cut @@ -524,27 +770,56 @@ sub count_literal { shift->search_literal(@_)->count; } =head2 all -Returns all elements in the resultset. Called implictly if the resultset +=over 4 + +=item Arguments: none + +=item Return Value: @objects + +=back + +Returns all elements in the resultset. Called implicitly if the resultset is returned in list context. =cut sub all { my ($self) = @_; - return @{ $self->get_cache } - if @{ $self->get_cache }; - if( $self->{attrs}->{cache} ) { - my @obj = map { $self->_construct_object(@$_); } - $self->cursor->all; - $self->set_cache( \@obj ); - return @{ $self->get_cache }; + return @{ $self->get_cache } if @{ $self->get_cache }; + + my @obj; + + if (keys %{$self->{collapse}}) { + # Using $self->cursor->all is really just an optimisation. + # If we're collapsing has_many prefetches it probably makes + # very little difference, and this is cleaner than hacking + # _construct_object to survive the approach + $self->cursor->reset; + my @row = $self->cursor->next; + while (@row) { + push(@obj, $self->_construct_object(@row)); + @row = (exists $self->{stashed_row} + ? @{delete $self->{stashed_row}} + : $self->cursor->next); + } + } else { + @obj = map { $self->_construct_object(@$_) } $self->cursor->all; } - return map { $self->_construct_object(@$_); } - $self->cursor->all; + + $self->set_cache(\@obj) if $self->{attrs}{cache}; + return @obj; } =head2 reset +=over 4 + +=item Arguments: none + +=item Return Value: $self + +=back + Resets the resultset's cursor, so you can iterate through the elements again. =cut @@ -558,7 +833,16 @@ sub reset { =head2 first -Resets the resultset and returns the first element. +=over 4 + +=item Arguments: none + +=item Return Value: $object? + +=back + +Resets the resultset and returns an object for the first result (if the +resultset returns anything). =cut @@ -566,29 +850,118 @@ sub first { return $_[0]->reset->next; } -=head2 update(\%values) +# _cond_for_update_delete +# +# update/delete require the condition to be modified to handle +# the differing SQL syntax available. This transforms the $self->{cond} +# appropriately, returning the new condition. + +sub _cond_for_update_delete { + my ($self) = @_; + my $cond = {}; + + if (!ref($self->{cond})) { + # No-op. No condition, we're updating/deleting everything + } + elsif (ref $self->{cond} eq 'ARRAY') { + $cond = [ + map { + my %hash; + foreach my $key (keys %{$_}) { + $key =~ /([^.]+)$/; + $hash{$1} = $_->{$key}; + } + \%hash; + } @{$self->{cond}} + ]; + } + elsif (ref $self->{cond} eq 'HASH') { + if ((keys %{$self->{cond}})[0] eq '-and') { + $cond->{-and} = []; + + my @cond = @{$self->{cond}{-and}}; + for (my $i = 0; $i < @cond - 1; $i++) { + my $entry = $cond[$i]; + + my %hash; + if (ref $entry eq 'HASH') { + foreach my $key (keys %{$entry}) { + $key =~ /([^.]+)$/; + $hash{$1} = $entry->{$key}; + } + } + else { + $entry =~ /([^.]+)$/; + $hash{$entry} = $cond[++$i]; + } + + push @{$cond->{-and}}, \%hash; + } + } + else { + foreach my $key (keys %{$self->{cond}}) { + $key =~ /([^.]+)$/; + $cond->{$1} = $self->{cond}{$key}; + } + } + } + else { + $self->throw_exception( + "Can't update/delete on resultset with condition unless hash or array" + ); + } + + return $cond; +} + + +=head2 update + +=over 4 + +=item Arguments: \%values -Sets the specified columns in the resultset to the supplied values. +=item Return Value: $storage_rv + +=back + +Sets the specified columns in the resultset to the supplied values in a +single query. Return value will be true if the update succeeded or false +if no records were updated; exact type of success value is storage-dependent. =cut sub update { my ($self, $values) = @_; - $self->throw_exception("Values for update must be a hash") unless ref $values eq 'HASH'; + $self->throw_exception("Values for update must be a hash") + unless ref $values eq 'HASH'; + + my $cond = $self->_cond_for_update_delete; + return $self->result_source->storage->update( - $self->result_source->from, $values, $self->{cond}); + $self->result_source->from, $values, $cond + ); } -=head2 update_all(\%values) +=head2 update_all + +=over 4 -Fetches all objects and updates them one at a time. Note that C -will run cascade triggers while L will not. +=item Arguments: \%values + +=item Return Value: 1 + +=back + +Fetches all objects and updates them one at a time. Note that C +will run DBIC cascade triggers, while L will not. =cut sub update_all { my ($self, $values) = @_; - $self->throw_exception("Values for update must be a hash") unless ref $values eq 'HASH'; + $self->throw_exception("Values for update must be a hash") + unless ref $values eq 'HASH'; foreach my $obj ($self->all) { $obj->set_columns($values)->update; } @@ -597,41 +970,42 @@ sub update_all { =head2 delete -Deletes the contents of the resultset from its result source. +=over 4 + +=item Arguments: none + +=item Return Value: 1 + +=back + +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. =cut sub delete { my ($self) = @_; my $del = {}; - $self->throw_exception("Can't delete on resultset with condition unless hash or array") - unless (ref($self->{cond}) eq 'HASH' || ref($self->{cond}) eq 'ARRAY'); - if (ref $self->{cond} eq 'ARRAY') { - $del = [ map { my %hash; - foreach my $key (keys %{$_}) { - $key =~ /([^\.]+)$/; - $hash{$1} = $_->{$key}; - }; \%hash; } @{$self->{cond}} ]; - } elsif ((keys %{$self->{cond}})[0] eq '-and') { - $del->{-and} = [ map { my %hash; - foreach my $key (keys %{$_}) { - $key =~ /([^\.]+)$/; - $hash{$1} = $_->{$key}; - }; \%hash; } @{$self->{cond}{-and}} ]; - } else { - foreach my $key (keys %{$self->{cond}}) { - $key =~ /([^\.]+)$/; - $del->{$1} = $self->{cond}{$key}; - } - } - $self->result_source->storage->delete($self->result_source->from, $del); + + my $cond = $self->_cond_for_update_delete; + + $self->result_source->storage->delete($self->result_source->from, $cond); return 1; } =head2 delete_all -Fetches all objects and deletes them one at a time. Note that C -will run cascade triggers while L will not. +=over 4 + +=item Arguments: none + +=item Return Value: 1 + +=back + +Fetches all objects and deletes them one at a time. Note that C +will run DBIC cascade triggers, while L will not. =cut @@ -643,7 +1017,15 @@ sub delete_all { =head2 pager -Returns a L object for the current resultset. Only makes +=over 4 + +=item Arguments: none + +=item Return Value: $pager + +=back + +Return Value a L object for the current resultset. Only makes sense for queries with a C attribute. =cut @@ -651,16 +1033,26 @@ sense for queries with a C attribute. sub pager { my ($self) = @_; my $attrs = $self->{attrs}; - $self->throw_exception("Can't create pager for non-paged rs") unless $self->{page}; + $self->throw_exception("Can't create pager for non-paged rs") + unless $self->{page}; $attrs->{rows} ||= 10; - $self->count; return $self->{pager} ||= Data::Page->new( - $self->{count}, $attrs->{rows}, $self->{page}); + $self->_count, $attrs->{rows}, $self->{page}); } -=head2 page($page_num) +=head2 page + +=over 4 + +=item Arguments: $page_number + +=item Return Value: $rs -Returns a new resultset for the specified page. +=back + +Returns a resultset for the $page_number page of the resultset on which page +is called, where each page contains a number of rows equal to the 'rows' +attribute set on the resultset (10 by default). =cut @@ -671,9 +1063,17 @@ sub page { return (ref $self)->new($self->result_source, $attrs); } -=head2 new_result(\%vals) +=head2 new_result + +=over 4 + +=item Arguments: \%vals -Creates a result in the resultset's result class. +=item Return Value: $object + +=back + +Creates an object in the resultset's result class and returns it. =cut @@ -681,21 +1081,56 @@ sub new_result { my ($self, $values) = @_; $self->throw_exception( "new_result needs a hash" ) unless (ref $values eq 'HASH'); - $self->throw_exception( "Can't abstract implicit construct, condition not a hash" ) - if ($self->{cond} && !(ref $self->{cond} eq 'HASH')); + $self->throw_exception( + "Can't abstract implicit construct, condition not a hash" + ) if ($self->{cond} && !(ref $self->{cond} eq 'HASH')); my %new = %$values; my $alias = $self->{attrs}{alias}; foreach my $key (keys %{$self->{cond}||{}}) { - $new{$1} = $self->{cond}{$key} if ($key =~ m/^(?:$alias\.)?([^\.]+)$/); + $new{$1} = $self->{cond}{$key} if ($key =~ m/^(?:\Q${alias}.\E)?([^.]+)$/); } - my $obj = $self->result_source->result_class->new(\%new); + my $obj = $self->result_class->new(\%new); $obj->result_source($self->result_source) if $obj->can('result_source'); - $obj; + return $obj; } -=head2 create(\%vals) +=head2 find_or_new + +=over 4 -Inserts a record into the resultset and returns the object. +=item Arguments: \%vals, \%attrs? + +=item Return Value: $object + +=back + +Find an existing record from this resultset. If none exists, instantiate a new +result object and return it. The object will not be saved into your storage +until you call L on it. + +If you want objects to be saved immediately, use L instead. + +=cut + +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); +} + +=head2 create + +=over 4 + +=item Arguments: \%vals + +=item Return Value: $object + +=back + +Inserts a record into the resultset and returns the object representing it. Effectively a shortcut for C<< ->new_result(\%vals)->insert >>. @@ -703,16 +1138,25 @@ Effectively a shortcut for C<< ->new_result(\%vals)->insert >>. sub create { my ($self, $attrs) = @_; - $self->throw_exception( "create needs a hashref" ) unless ref $attrs eq 'HASH'; + $self->throw_exception( "create needs a hashref" ) + unless ref $attrs eq 'HASH'; return $self->new_result($attrs)->insert; } -=head2 find_or_create(\%vals, \%attrs?) +=head2 find_or_create + +=over 4 + +=item Arguments: \%vals, \%attrs? + +=item Return Value: $object + +=back $class->find_or_create({ key => $val, ... }); -Searches for a record matching the search condition; if it doesn't find one, -creates one and returns that instead. +Searches for a record matching the search condition; if it doesn't find one, +creates one and returns that instead. my $cd = $schema->resultset('CD')->find_or_create({ cdid => 5, @@ -732,25 +1176,34 @@ constraint. For example: { key => 'artist_title' } ); -See also L and L. +See also L and L. For information on how to declare +unique constraints, see L. =cut sub find_or_create { my $self = shift; my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); - my $hash = ref $_[0] eq "HASH" ? shift : {@_}; + my $hash = ref $_[0] eq 'HASH' ? shift : {@_}; my $exists = $self->find($hash, $attrs); - return defined($exists) ? $exists : $self->create($hash); + return defined $exists ? $exists : $self->create($hash); } =head2 update_or_create - $class->update_or_create({ key => $val, ... }); +=over 4 + +=item Arguments: \%col_values, { key => $unique_constraint }? + +=item Return Value: $object -First, search for an existing row matching one of the unique constraints -(including the primary key) on the source of this resultset. If a row is -found, update it with the other given column values. Otherwise, create a new +=back + + $class->update_or_create({ col => $val, ... }); + +First, searches for an existing row matching one of the unique constraints +(including the primary key) on the source of this resultset. If a row is +found, updates it with the other given column values. Otherwise, creates a new row. Takes an optional C attribute to search on a specific unique constraint. @@ -769,65 +1222,60 @@ For example: If no C is specified, it searches on all unique constraints defined on the source, including the primary key. -If the C is specified as C, search only on the primary key. +If the C is specified as C, it searches only on the primary key. -See also L and L. +See also L and L. For information on how to declare +unique constraints, see L. =cut sub update_or_create { my $self = shift; - my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); - my $hash = ref $_[0] eq "HASH" ? shift : {@_}; - - my %unique_constraints = $self->result_source->unique_constraints; - my @constraint_names = (exists $attrs->{key} - ? ($attrs->{key}) - : keys %unique_constraints); - - my @unique_hashes; - foreach my $name (@constraint_names) { - my @unique_cols = @{ $unique_constraints{$name} }; - my %unique_hash = - map { $_ => $hash->{$_} } - grep { exists $hash->{$_} } - @unique_cols; - - push @unique_hashes, \%unique_hash - if (scalar keys %unique_hash == scalar @unique_cols); - } - - my $row; - if (@unique_hashes) { - $row = $self->search(\@unique_hashes, { rows => 1 })->first; - if ($row) { - $row->set_columns($hash); - $row->update; - } - } + my $hash = ref $_[0] eq 'HASH' ? shift : {@_}; - unless ($row) { - $row = $self->create($hash); + my $row = $self->find($hash, $attrs); + if (defined $row) { + $row->set_columns($hash); + $row->update; + return $row; } - return $row; + return $self->create($hash); } =head2 get_cache -Gets the contents of the cache for the resultset. +=over 4 + +=item Arguments: none + +=item Return Value: \@cache_objects? + +=back + +Gets the contents of the cache for the resultset, if the cache is set. =cut sub get_cache { - my $self = shift; - return $self->{all_cache} || []; + shift->{all_cache} || []; } =head2 set_cache -Sets the contents of the cache for the resultset. Expects an arrayref of objects of the same class as those produced by the resultset. +=over 4 + +=item Arguments: \@cache_objects + +=item Return Value: \@cache_objects + +=back + +Sets the contents of the cache for the resultset. Expects an arrayref +of objects of the same class as those produced by the resultset. Note that +if the cache is set the resultset will return the cached objects rather +than re-querying the database even if the cache attr is not set. =cut @@ -835,72 +1283,79 @@ sub set_cache { my ( $self, $data ) = @_; $self->throw_exception("set_cache requires an arrayref") if ref $data ne 'ARRAY'; - my $result_class = $self->result_source->result_class; + my $result_class = $self->result_class; foreach( @$data ) { - $self->throw_exception("cannot cache object of type '$_', expected '$result_class'") - if ref $_ ne $result_class; + $self->throw_exception( + "cannot cache object of type '$_', expected '$result_class'" + ) if ref $_ ne $result_class; } $self->{all_cache} = $data; } =head2 clear_cache +=over 4 + +=item Arguments: none + +=item Return Value: [] + +=back + Clears the cache for the resultset. =cut sub clear_cache { - my $self = shift; - $self->set_cache([]); + shift->set_cache([]); } =head2 related_resultset +=over 4 + +=item Arguments: $relationship_name + +=item Return Value: $resultset + +=back + Returns a related resultset for the supplied relationship name. - $rs = $rs->related_resultset('foo'); + $artist_rs = $schema->resultset('CD')->related_resultset('Artist'); =cut sub related_resultset { - my ( $self, $rel, @rest ) = @_; + my ( $self, $rel ) = @_; $self->{related_resultsets} ||= {}; - my $resultsets = $self->{related_resultsets}; - if( !exists $resultsets->{$rel} ) { - #warn "fetching related resultset for rel '$rel'"; - my $rel_obj = $self->result_source->relationship_info($rel); - $self->throw_exception( - "search_related: result source '" . $self->result_source->name . - "' has no such relationship ${rel}") - unless $rel_obj; #die Dumper $self->{attrs}; - my $rs; - if( $self->{attrs}->{cache} ) { - $rs = $self->search(undef); - } - else { - $rs = $self->search(undef, { join => $rel }); - } - #use Data::Dumper; die Dumper $rs->{attrs};#$rs = $self->search( undef ); - #use Data::Dumper; warn Dumper $self->{attrs}, Dumper $rs->{attrs}; - my $alias = (defined $rs->{attrs}{seen_join}{$rel} - && $rs->{attrs}{seen_join}{$rel} > 1 - ? join('_', $rel, $rs->{attrs}{seen_join}{$rel}) - : $rel); - $resultsets->{$rel} = + return $self->{related_resultsets}{$rel} ||= do { + #warn "fetching related resultset for rel '$rel'"; + my $rel_obj = $self->result_source->relationship_info($rel); + $self->throw_exception( + "search_related: result source '" . $self->result_source->name . + "' has no such relationship ${rel}") + unless $rel_obj; #die Dumper $self->{attrs}; + + my $rs = $self->search(undef, { join => $rel }); + my $alias = defined $rs->{attrs}{seen_join}{$rel} + && $rs->{attrs}{seen_join}{$rel} > 1 + ? join('_', $rel, $rs->{attrs}{seen_join}{$rel}) + : $rel; + $self->result_source->schema->resultset($rel_obj->{class} )->search( undef, { %{$rs->{attrs}}, alias => $alias, - select => undef(), - as => undef() } - )->search(@rest); - } - return $resultsets->{$rel}; + select => undef, + as => undef } + ); + }; } =head2 throw_exception -See Schema's throw_exception +See L for details. =cut @@ -909,6 +1364,8 @@ sub throw_exception { $self->result_source->schema->throw_exception(@_); } +# XXX: FIXME: Attributes docs need clearing up + =head1 ATTRIBUTES The resultset takes various attributes that modify its behavior. Here's an @@ -916,79 +1373,115 @@ overview of them: =head2 order_by -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. +=over 4 -=head2 cols (arrayref) +=item Value: ($order_by | \@order_by) + +=back + +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'. + +=head2 columns + +=over 4 + +=item Value: \@columns + +=back Shortcut to request a particular set of columns to be retrieved. Adds C onto the start of any column without a C<.> in it and sets C as normal. +from that, then auto-populates C from C, usually when C