X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FResultSet.pm;h=a33e22f5b80beed3d811447bfee0ed429a5fc28a;hb=8452e496;hp=eae9193ea0ea517f107e11d8607c281b40ef71ef;hpb=df8abd99a70d8d8af5046da841557e1a394b752b;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/ResultSet.pm b/lib/DBIx/Class/ResultSet.pm index eae9193..a33e22f 100644 --- a/lib/DBIx/Class/ResultSet.pm +++ b/lib/DBIx/Class/ResultSet.pm @@ -6,6 +6,7 @@ use overload '0+' => 'count', fallback => 1; use Data::Page; +use Storable; =head1 NAME @@ -24,71 +25,204 @@ or a C relationship. =head1 METHODS -=head2 new($db_class, \%$attrs) +=head2 new($source, \%$attrs) -The resultset constructor. Takes a table class and an attribute hash -(see below for more information on attributes). Does not perform -any queries -- these are executed as needed by the other methods. +The resultset constructor. Takes a source object (usually a DBIx::Class::Table) +and an attribute hash (see below for more information on attributes). Does +not perform any queries -- these are executed as needed by the other methods. =cut sub new { - my ($it_class, $db_class, $attrs) = @_; + my $class = shift; + $class->new_result(@_) if ref $class; + my ($source, $attrs) = @_; #use Data::Dumper; warn Dumper(@_); - $it_class = ref $it_class if ref $it_class; - $attrs = { %{ $attrs || {} } }; + $attrs = Storable::dclone($attrs || {}); # { %{ $attrs || {} } }; my %seen; - $attrs->{cols} ||= [ map { "me.$_" } $db_class->_select_columns ]; - $attrs->{from} ||= [ { 'me' => $db_class->_table_name } ]; - if ($attrs->{join}) { - foreach my $j (ref $attrs->{join} eq 'ARRAY' - ? (@{$attrs->{join}}) : ($attrs->{join})) { + my $alias = ($attrs->{alias} ||= 'me'); + if (!$attrs->{select}) { + my @cols = ($attrs->{cols} + ? @{delete $attrs->{cols}} + : $source->result_class->_select_columns); + $attrs->{select} = [ map { m/\./ ? $_ : "${alias}.$_" } @cols ]; + } + $attrs->{as} ||= [ map { m/^$alias\.(.*)$/ ? $1 : $_ } @{$attrs->{select}} ]; + #use Data::Dumper; warn Dumper(@{$attrs}{qw/select as/}); + $attrs->{from} ||= [ { $alias => $source->from } ]; + if (my $join = delete $attrs->{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}}, $db_class->_resolve_join($attrs->{join}, 'me')); + push(@{$attrs->{from}}, $source->resolve_join($join, $attrs->{alias})); } - foreach my $pre (@{$attrs->{prefetch} || []}) { - push(@{$attrs->{from}}, $db_class->_resolve_join($pre, 'me')) + $attrs->{group_by} ||= $attrs->{select} if delete $attrs->{distinct}; + foreach my $pre (@{delete $attrs->{prefetch} || []}) { + push(@{$attrs->{from}}, $source->resolve_join($pre, $attrs->{alias})) unless $seen{$pre}; - push(@{$attrs->{cols}}, + my @pre = map { "$pre.$_" } - $db_class->_relationships->{$pre}->{class}->_select_columns); + $source->result_class->relationship_info($pre)->{class}->columns; + push(@{$attrs->{select}}, @pre); + push(@{$attrs->{as}}, @pre); + } + if ($attrs->{page}) { + $attrs->{rows} ||= 10; + $attrs->{offset} ||= 0; + $attrs->{offset} += ($attrs->{rows} * ($attrs->{page} - 1)); } my $new = { - class => $db_class, - cols => $attrs->{cols} || [ $db_class->_select_columns ], + source => $source, cond => $attrs->{where}, - from => $attrs->{from} || $db_class->_table_name, + from => $attrs->{from}, count => undef, + page => delete $attrs->{page}, pager => undef, attrs => $attrs }; - bless ($new, $it_class); - $new->pager if ($attrs->{page}); + bless ($new, $class); return $new; } +=head2 search + + my @obj = $rs->search({ foo => 3 }); # "... WHERE foo = 3" + my $new_rs = $rs->search({ foo => 3 }); + +If you need to pass in additional attributes but no additional condition, +call it as ->search(undef, \%attrs); + + my @all = $class->search({}, { cols => [qw/foo bar/] }); # "SELECT foo, bar FROM $class_table" + +=cut + +sub search { + my $self = shift; + + #use Data::Dumper;warn Dumper(@_); + + 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' => [ $where, $attrs->{where} ] } + : $where); + $attrs->{where} = $where; + } + + my $rs = (ref $self)->new($self->{source}, $attrs); + + 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); + +Pass a literal chunk of SQL to be added to the conditional part of the +resultset + +=cut + +sub search_literal { + my ($self, $cond, @vals) = @_; + my $attrs = (ref $vals[$#vals] eq 'HASH' ? { %{ pop(@vals) } } : {}); + $attrs->{bind} = [ @{$self->{attrs}{bind}||[]}, @vals ]; + return $self->search(\$cond, $attrs); +} + +=head2 find(@colvalues), find(\%cols) + +Finds a row based on its primary key(s). + +=cut + +sub find { + my ($self, @vals) = @_; + my $attrs = (@vals > 1 && ref $vals[$#vals] eq 'HASH' ? pop(@vals) : {}); + my @pk = $self->{source}->primary_columns; + #use Data::Dumper; warn Dumper($attrs, @vals, @pk); + $self->{source}->result_class->throw( "Can't find unless primary columns are defined" ) + unless @pk; + my $query; + if (ref $vals[0] eq 'HASH') { + $query = $vals[0]; + } elsif (@pk == @vals) { + $query = {}; + @{$query}{@pk} = @vals; + } else { + $query = {@vals}; + } + #warn Dumper($query); + # Useless -> disabled + #$self->{source}->result_class->throw( "Can't find unless all primary keys are specified" ) + # unless (keys %$query >= @pk); # If we check 'em we run afoul of uc/lc + # column names etc. Not sure what to do yet + return $self->search($query)->next; +} + +=head2 search_related + + $rs->search_related('relname', $cond?, $attrs?); + +=cut + +sub search_related { + my ($self, $rel, @rest) = @_; + my $rel_obj = $self->{source}->result_class->relationship_info($rel); + $self->{source}->result_class->throw( + "No such relationship ${rel} in search_related") + unless $rel_obj; + my $rs = $self->search(undef, { join => $rel }); + return $self->{source}->schema->resultset($rel_obj->{class} + )->search( undef, + { %{$rs->{attrs}}, + alias => $rel, + select => undef(), + as => undef() } + )->search(@rest); +} + =head2 cursor -Return a storage-driven cursor to the given resultset. +Returns a storage-driven cursor to the given resultset. =cut sub cursor { my ($self) = @_; - my ($db_class, $attrs) = @{$self}{qw/class attrs/}; - if ($attrs->{page}) { - $attrs->{rows} = $self->pager->entries_per_page; - $attrs->{offset} = $self->pager->skipped; - } + my ($source, $attrs) = @{$self}{qw/source attrs/}; + $attrs = { %$attrs }; return $self->{cursor} - ||= $db_class->storage->select($self->{from}, $self->{cols}, + ||= $source->storage->select($self->{from}, $attrs->{select}, $attrs->{where},$attrs); } +=head2 search_like + +Identical to search except defaults to 'LIKE' instead of '=' in condition + +=cut + +sub search_like { + my $class = shift; + my $attrs = { }; + if (@_ > 1 && ref $_[$#_] eq 'HASH') { + $attrs = pop(@_); + } + my $query = ref $_[0] eq "HASH" ? { %{shift()} }: {@_}; + $query->{$_} = { 'like' => $query->{$_} } for keys %$query; + return $class->search($query, { %$attrs }); +} + =head2 slice($first, $last) Returns a subset of elements from the resultset. @@ -98,10 +232,10 @@ Returns a subset of elements from the resultset. sub slice { my ($self, $min, $max) = @_; my $attrs = { %{ $self->{attrs} || {} } }; - $self->{class}->throw("Can't slice without where") unless $attrs->{where}; - $attrs->{offset} = $min; + $attrs->{offset} ||= 0; + $attrs->{offset} += $min; $attrs->{rows} = ($max ? ($max - $min + 1) : 1); - my $slice = $self->new($self->{class}, $attrs); + my $slice = (ref $self)->new($self->{source}, $attrs); return (wantarray ? $slice->all : $slice); } @@ -120,38 +254,17 @@ sub next { sub _construct_object { my ($self, @row) = @_; - my @cols = @{ $self->{attrs}{cols} }; - s/^me\.// for @cols; - @cols = grep { /\(/ or ! /\./ } @cols; - my $new; - unless ($self->{attrs}{prefetch}) { - $new = $self->{class}->_row_to_object(\@cols, \@row); - } else { - my @main = splice(@row, 0, scalar @cols); - $new = $self->{class}->_row_to_object(\@cols, \@main); - PRE: foreach my $pre (@{$self->{attrs}{prefetch}}) { - my $rel_obj = $self->{class}->_relationships->{$pre}; - my $pre_class = $self->{class}->resolve_class($rel_obj->{class}); - my @pre_cols = $pre_class->_select_columns; - my @vals = splice(@row, 0, scalar @pre_cols); - my $fetched = $pre_class->_row_to_object(\@pre_cols, \@vals); - $self->{class}->throw("No accessor for prefetched $pre") - unless defined $rel_obj->{attrs}{accessor}; - if ($rel_obj->{attrs}{accessor} eq 'single') { - foreach my $pri ($rel_obj->{class}->primary_columns) { - unless (defined $fetched->get_column($pri)) { - undef $fetched; - last; - } - } - $new->{_relationship_data}{$pre} = $fetched; - } elsif ($rel_obj->{attrs}{accessor} eq 'filter') { - $new->{_inflated_column}{$pre} = $fetched; - } else { - $self->{class}->throw("Don't know how to store prefetched $pre"); - } + my @cols = @{ $self->{attrs}{as} }; + #warn "@cols -> @row"; + my (%me, %pre); + foreach my $col (@cols) { + if ($col =~ /([^\.]+)\.([^\.]+)/) { + $pre{$1}[0]{$2} = shift @row; + } else { + $me{$col} = shift @row; } } + my $new = $self->{source}->result_class->inflate_result(\%me, \%pre); $new = $self->{attrs}{record_filter}->($new) if exists $self->{attrs}{record_filter}; return $new; @@ -160,29 +273,40 @@ sub _construct_object { =head2 count Performs an SQL C with the same query as the resultset was built -with to find the number of elements. +with to find the number of elements. If passed arguments, does a search +on the resultset and counts the results of that. =cut sub count { - my ($self) = @_; - my $db_class = $self->{class}; - my $attrs = { %{ $self->{attrs} } }; - unless ($self->{count}) { - # offset and order by are not needed to count - delete $attrs->{$_} for qw/offset order_by/; + my $self = shift; + return $self->search(@_)->count if @_ && defined $_[0]; + die "Unable to ->count with a GROUP BY" if defined $self->{attrs}{group_by}; + unless (defined $self->{count}) { + my $attrs = { %{ $self->{attrs} }, + select => { 'count' => '*' }, + 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/; - my @cols = 'COUNT(*)'; - $self->{count} = $db_class->storage->select_single($self->{from}, \@cols, - $self->{cond}, $attrs); + ($self->{count}) = (ref $self)->new($self->{source}, $attrs)->cursor->next; } return 0 unless $self->{count}; - return $self->{pager}->entries_on_this_page if ($self->{pager}); - return ( $attrs->{rows} && $attrs->{rows} < $self->{count} ) - ? $attrs->{rows} - : $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); + return $count; } +=head2 count_literal + +Calls search_literal with the passed arguments, then count. + +=cut + +sub count_literal { shift->search_literal(@_)->count; } + =head2 all Returns all elements in the resultset. Called implictly if the resultset @@ -242,12 +366,11 @@ sense for queries with page turned on. sub pager { my ($self) = @_; my $attrs = $self->{attrs}; - delete $attrs->{offset}; - my $rows_per_page = delete $attrs->{rows} || 10; - $self->{pager} ||= Data::Page->new( - $self->count, $rows_per_page, $attrs->{page} || 1); - $attrs->{rows} = $rows_per_page; - return $self->{pager}; + die "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}); } =head2 page($page_num) @@ -258,12 +381,62 @@ Returns a new resultset for the specified page. sub page { my ($self, $page) = @_; - my $attrs = $self->{attrs}; + my $attrs = { %{$self->{attrs}} }; $attrs->{page} = $page; - return $self->new($self->{class}, $attrs); + return (ref $self)->new($self->{source}, $attrs); } -=head1 Attributes +=head2 new_result(\%vals) + +Creates a result in the resultset's result class + +=cut + +sub new_result { + my ($self, $values) = @_; + $self->{source}->result_class->throw( "new_result needs a hash" ) + unless (ref $values eq 'HASH'); + $self->{source}->result_class->throw( "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\.)?([^\.]+)$/); + } + return $self->{source}->result_class->new(\%new); +} + +=head2 create(\%vals) + +Inserts a record into the resultset and returns the object + +Effectively a shortcut for ->new_result(\%vals)->insert + +=cut + +sub create { + my ($self, $attrs) = @_; + $self->{source}->result_class->throw( "create needs a hashref" ) unless ref $attrs eq 'HASH'; + return $self->new_result($attrs)->insert; +} + +=head2 find_or_create(\%vals) + + $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. + +=cut + +sub find_or_create { + my $self = shift; + my $hash = ref $_[0] eq "HASH" ? shift: {@_}; + my $exists = $self->find($hash); + return defined($exists) ? $exists : $self->create($hash); +} + +=head1 ATTRIBUTES The resultset takes various attributes that modify its behavior. Here's an overview of them: @@ -274,13 +447,23 @@ 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. -=head2 cols +=head2 cols (arrayref) + +Shortcut to request a particular set of columns to be retrieved - adds +'me.' onto the start of any column without a '.' in it and sets 'select' +from that, then auto-populates 'as' from 'select' as normal -Which columns should be retrieved. +=head2 select (arrayref) + +Indicates which columns should be selected from the storage + +=head2 as (arrayref) + +Indicates column names for object inflation =head2 join -Contains a list of relations that should be joined for this query. Can also +Contains a list of relationships that should be joined for this query. Can also contain a hash reference to refer to that relation's relations. So, if one column in your class C foo and another C bar, you can do C<< join => [qw/ foo bar /] >> to join both (and e.g. use them for C). @@ -288,6 +471,14 @@ If a foo contains many margles and you want to join those too, you can do C<< join => { foo => 'margle' } >>. If you want to fetch the columns from the related table as well, see C below. +=head2 prefetch + +Contains a list of relationships that should be fetched along with the main +query (when they are accessed afterwards they will have already been +"prefetched"). This is useful for when you know you will need the related +object(s), because it saves a query. Currently limited to prefetching +one relationship deep, so unlike C, prefetch must be an arrayref. + =head2 from This attribute can contain a arrayref of elements. Each element can be another @@ -305,6 +496,15 @@ for an unpaged resultset. For a paged resultset, how many rows per page +=head2 group_by + +A list of columns to group by (note that 'count' doesn't work on grouped +resultsets) + +=head2 distinct + +Set to 1 to group by all columns + =cut 1;