X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FResultSet.pm;h=da955a47a339ab1e9f39c482977008d217ec7c8c;hb=7ed3d6dc7d83264aa67108ec304985c0116d6009;hp=ce038dbbfbb9a46db53ea2018f7b5bd3e397707d;hpb=cda04c3afd46989e1964a6c8a277fd7faa11b291;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/ResultSet.pm b/lib/DBIx/Class/ResultSet.pm index ce038db..da955a4 100644 --- a/lib/DBIx/Class/ResultSet.pm +++ b/lib/DBIx/Class/ResultSet.pm @@ -3,9 +3,16 @@ 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 base qw/DBIx::Class/; +__PACKAGE__->load_components(qw/AccessorGroup/); +__PACKAGE__->mk_group_accessors('simple' => qw/result_source result_class/); =head1 NAME @@ -13,109 +20,201 @@ DBIx::Class::ResultSet - Responsible for fetching and creating resultset. =head1 SYNOPSIS -my $rs = MyApp::DB::Class->search(registered => 1); -my @rows = MyApp::DB::Class->search(foo => 'bar'); + my $rs = $schema->resultset('User')->search(registered => 1); + my @rows = $schema->resultset('Foo')->search(bar => 'baz'); =head1 DESCRIPTION The resultset is also known as an iterator. It is responsible for handling -queries that may return an arbitrary number of rows, e.g. via C +queries that may return an arbitrary number of rows, e.g. via L or a C relationship. +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'); + __PACKAGE__->has_many(cds => 'MyApp::Schema::CD'); + 1; + + package MyApp::Schema::CD; + use base qw/DBIx::Class/; + __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'); + 1; + =head1 METHODS -=head2 new($db_class, \%$attrs) +=head2 new + +=head3 Arguments: ($source, \%$attrs) + +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. + +Generally you won't need to construct a resultset manually. You'll +automatically get one from e.g. a L called in scalar context: -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. + my $rs = $schema->resultset('CD')->search({ title => '100th Window' }); =cut sub new { - my ($class, $source, $attrs) = @_; - #use Data::Dumper; warn Dumper(@_); - $class = ref $class if ref $class; - $attrs = { %{ $attrs || {} } }; + my $class = shift; + return $class->new_result(@_) if ref $class; + + my ($source, $attrs) = @_; + weaken $source; + $attrs = Storable::dclone($attrs || {}); # { %{ $attrs || {} } }; + #use Data::Dumper; warn Dumper($attrs); + my $alias = ($attrs->{alias} ||= 'me'); + + $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); + } + #use Data::Dumper; warn Dumper(@{$attrs}{qw/select as/}); + + $attrs->{from} ||= [ { $alias => $source->from } ]; + $attrs->{seen_join} ||= {}; my %seen; - $attrs->{cols} ||= [ map { "me.$_" } $source->columns ]; - $attrs->{from} ||= [ { 'me' => $source->name } ]; - if ($attrs->{join}) { - foreach my $j (ref $attrs->{join} eq 'ARRAY' - ? (@{$attrs->{join}}) : ($attrs->{join})) { + 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}}, $source->result_class->_resolve_join($attrs->{join}, 'me')); + 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}) { + 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 { + push(@{$attrs->{from}}, $source->resolve_join($p, $attrs->{alias})) + unless $seen{$p}; + } + 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); } - foreach my $pre (@{$attrs->{prefetch} || []}) { - push(@{$attrs->{from}}, $source->result_class->_resolve_join($pre, 'me')) - unless $seen{$pre}; - push(@{$attrs->{cols}}, - map { "$pre.$_" } - $source->result_class->_relationships->{$pre}->{class}->table->columns); + $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 = { - source => $source, - result_class => $source->result_class, - cols => $attrs->{cols}, + + 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); - $new->pager if ($attrs->{page}); - return $new; + attrs => $attrs + }, $class; } =head2 search - my @obj = $rs->search({ foo => 3 }); # "... WHERE foo = 3" - my $new_rs = $rs->search({ foo => 3 }); - + 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" +call it as C. + + # "SELECT foo, bar FROM $class_table" + my @all = $class->search(undef, { columns => [qw/foo bar/] }); =cut sub search { my $self = shift; - #use Data::Dumper;warn Dumper(@_); - - my $attrs = { %{$self->{attrs}} }; - if (@_ > 1 && ref $_[$#_] eq 'HASH') { - $attrs = { %{ pop(@_) } }; - } - - my $where = ((@_ == 1 || ref $_[0] eq "HASH") ? shift : {@_}); - if (defined $where) { - $where = (defined $attrs->{where} - ? { '-and' => [ $where, $attrs->{where} ] } + my $rs; + if( @_ ) { + + 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); - $attrs->{where} = $where; - } + } - my $rs = $self->new($self->{source}, $attrs); + if (defined $having) { + $attrs->{having} = (defined $attrs->{having} + ? { '-and' => + [ map { ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ } + $having, $attrs->{having} ] } + : $having); + } + $rs = (ref $self)->new($self->result_source, $attrs); + } + else { + $rs = $self; + $rs->reset; + } return (wantarray ? $rs->all : $rs); } -=head2 search_literal +=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 +resultset. =cut - + sub search_literal { my ($self, $cond, @vals) = @_; my $attrs = (ref $vals[$#vals] eq 'HASH' ? { %{ pop(@vals) } } : {}); @@ -123,6 +222,79 @@ sub search_literal { return $self->search(\$cond, $attrs); } +=head2 find + +=head3 Arguments: (@colvalues) | (\%cols, \%attrs?) + +Finds a row based on its primary key or unique constraint. For example: + + my $cd = $schema->resultset('CD')->find(5); + +Also takes an optional C attribute, to search by a specific key or unique +constraint. For example: + + my $cd = $schema->resultset('CD')->find( + { + artist => 'Massive Attack', + title => 'Mezzanine', + }, + { key => 'artist_title' } + ); + +See also L and 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->throw_exception( "Unknown key $attrs->{key} on $self->name" ) + unless exists $uniq{$attrs->{key}}; + @cols = @{ $uniq{$attrs->{key}} }; + } + #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}; + } + foreach my $key (grep { ! m/\./ } keys %$query) { + $query->{"$self->{attrs}{alias}.$key"} = delete $query->{$key}; + } + #warn Dumper($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); + } +} + +=head2 search_related + + $rs->search_related('relname', $cond?, $attrs?); + +Search the specified relationship. Optionally specify a condition for matching +records. + +=cut + +sub search_related { + return shift->related_resultset(shift)->search(@_); +} + =head2 cursor Returns a storage-driven cursor to the given resultset. @@ -131,34 +303,60 @@ Returns a storage-driven cursor to the given resultset. sub cursor { my ($self) = @_; - my ($source, $attrs) = @{$self}{qw/source attrs/}; - if ($attrs->{page}) { - $attrs->{rows} = $self->pager->entries_per_page; - $attrs->{offset} = $self->pager->skipped; - } + my $attrs = { %{$self->{attrs}} }; return $self->{cursor} - ||= $source->storage->select($self->{from}, $self->{cols}, + ||= $self->result_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 +=head2 single -sub search_like { - my $class = shift; - my $attrs = { }; - if (@_ > 1 && ref $_[$#_] eq 'HASH') { - $attrs = pop(@_); +Inflates the first result without creating a cursor + +=cut + +sub single { + my ($self, $where) = @_; + my $attrs = { %{$self->{attrs}} }; + if ($where) { + if (defined $attrs->{where}) { + $attrs->{where} = { + '-and' => + [ map { ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ } + $where, delete $attrs->{where} ] + }; + } else { + $attrs->{where} = $where; + } } - my $query = ref $_[0] eq "HASH" ? { %{shift()} }: {@_}; + my @data = $self->result_source->storage->select_single( + $self->{from}, $attrs->{select}, + $attrs->{where},$attrs); + return (@data ? $self->_construct_object(@data) : ()); +} + + +=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 +L with specific operators. + +For more information, see L. + +=cut + +sub search_like { + 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 + +=head3 Arguments: ($first, $last) Returns a subset of elements from the resultset. @@ -167,95 +365,192 @@ Returns a subset of elements from the resultset. sub slice { my ($self, $min, $max) = @_; my $attrs = { %{ $self->{attrs} || {} } }; - $self->{source}->result_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->{source}, $attrs); + my $slice = (ref $self)->new($self->result_source, $attrs); return (wantarray ? $slice->all : $slice); } -=head2 next +=head2 next + +Returns the next element in the resultset (C is there is none). -Returns the next element in the resultset (undef is there is none). +Can be used to efficiently iterate over records in the resultset: + + my $rs = $schema->resultset('CD')->search; + while (my $cd = $rs->next) { + print $cd->title; + } =cut sub next { my ($self) = @_; - my @row = $self->cursor->next; + if (@{$self->{all_cache} || []}) { + $self->{all_cache_position} ||= 0; + return $self->{all_cache}->[$self->{all_cache_position}++]; + } + if ($self->{attrs}{cache}) { + $self->{all_cache_position} = 1; + return ($self->all)[0]; + } + 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); } 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->{source}->result_class->_row_to_object(\@cols, \@row); - } else { - my @main = splice(@row, 0, scalar @cols); - $new = $self->{source}->result_class->_row_to_object(\@cols, \@main); - PRE: foreach my $pre (@{$self->{attrs}{prefetch}}) { - my $rel_obj = $self->{source}->result_class->_relationships->{$pre}; - my $pre_class = $self->{source}->result_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->{source}->result_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->{source}->result_class->throw("Don't know how to store prefetched $pre"); - } - } - } + my @as = @{ $self->{attrs}{as} }; + + 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 $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 = (defined($prefix) + ? (map { (m/^\Q${prefix}.\E(.+)$/ ? ($1) : ()); } + keys %{$self->{collapse}}) + : keys %{$self->{collapse}}); + if (@collapse) { + my ($c) = sort { length $a <=> length $b } @collapse; + my $target = $info; + foreach my $p (split(/\./, $c)) { + $target = $target->[1]->{$p} ||= []; + } + 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; + } + + return $info; +} + +=head2 result_source + +Returns a reference to the result source for this recordset. + +=cut + + =head2 count 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. +Note: When using C with C, L emulates C +using C. Some databases (notably SQLite) do +not support C with multiple columns. If you are using such a +database, you should only use columns from the main table in your C +clause. + =cut sub count { my $self = shift; - return $self->search(@_)->count if @_ && defined $_[0]; + return $self->search(@_)->count if @_ and defined $_[0]; + return scalar @{ $self->get_cache } if @{ $self->get_cache }; + + my $count = $self->_count; + return 0 unless $count; + + $count -= $self->{attrs}{offset} if $self->{attrs}{offset}; + $count = $self->{attrs}{rows} if + $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} } }; - unless ($self->{count}) { - # offset and order by are not needed to count - delete $attrs->{$_} for qw/offset order_by/; - - my @cols = 'COUNT(*)'; - $self->{count} = $self->{source}->storage->select_single( - $self->{from}, \@cols, $self->{cond}, $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; } - 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}; + + $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 search_literal with the passed arguments, then count. +Calls L with the passed arguments, then L. =cut @@ -270,8 +565,29 @@ is returned in list context. sub all { my ($self) = @_; - return map { $self->_construct_object(@$_); } - $self->cursor->all; + 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; + } + + $self->set_cache(\@obj) if $self->{attrs}{cache}; + return @obj; } =head2 reset @@ -282,6 +598,7 @@ Resets the resultset's cursor, so you can iterate through the elements again. sub reset { my ($self) = @_; + $self->{all_cache_position} = 0; $self->cursor->reset; return $self; } @@ -296,39 +613,119 @@ sub first { return $_[0]->reset->next; } +=head2 update + +=head3 Arguments: (\%values) + +Sets the specified columns in the resultset to the supplied values. + +=cut + +sub update { + my ($self, $values) = @_; + $self->throw_exception("Values for update must be a hash") unless ref $values eq 'HASH'; + return $self->result_source->storage->update( + $self->result_source->from, $values, $self->{cond}); +} + +=head2 update_all + +=head3 Arguments: (\%values) + +Fetches all objects and updates them one at a time. Note that C +will run 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'; + foreach my $obj ($self->all) { + $obj->set_columns($values)->update; + } + return 1; +} + =head2 delete -Deletes all elements in the resultset. +Deletes the contents of the resultset from its result source. =cut sub delete { my ($self) = @_; - $_->delete for $self->all; + my $del = {}; + + if (!ref($self->{cond})) { + + # No-op. No condition, we're deleting everything + + } elsif (ref $self->{cond} eq 'ARRAY') { + + $del = [ 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') { + + $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}; + } + } + } else { + $self->throw_exception( + "Can't delete on resultset with condition unless hash or array"); + } + + $self->result_source->storage->delete($self->result_source->from, $del); return 1; } -*delete_all = \&delete; # Yeah, yeah, yeah ... +=head2 delete_all + +Fetches all objects and deletes them one at a time. Note that C +will run cascade triggers while L will not. + +=cut + +sub delete_all { + my ($self) = @_; + $_->delete for $self->all; + return 1; +} =head2 pager Returns a L object for the current resultset. Only makes -sense for queries with page turned on. +sense for queries with a C attribute. =cut 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}; + $self->throw_exception("Can't create pager for non-paged rs") unless $self->{page}; + $attrs->{rows} ||= 10; + return $self->{pager} ||= Data::Page->new( + $self->_count, $attrs->{rows}, $self->{page}); } -=head2 page($page_num) +=head2 page + +=head3 Arguments: ($page_num) Returns a new resultset for the specified page. @@ -336,60 +733,524 @@ 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->{source}, $attrs); + return (ref $self)->new($self->result_source, $attrs); +} + +=head2 new_result + +=head3 Arguments: (\%vals) + +Creates a result in the resultset's result class. + +=cut + +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')); + my %new = %$values; + my $alias = $self->{attrs}{alias}; + foreach my $key (keys %{$self->{cond}||{}}) { + $new{$1} = $self->{cond}{$key} if ($key =~ m/^(?:\Q${alias}.\E)?([^.]+)$/); + } + my $obj = $self->result_class->new(\%new); + $obj->result_source($self->result_source) if $obj->can('result_source'); + return $obj; +} + +=head2 create + +=head3 Arguments: (\%vals) + +Inserts a record into the resultset and returns the object. + +Effectively a shortcut for C<< ->new_result(\%vals)->insert >>. + +=cut + +sub create { + my ($self, $attrs) = @_; + $self->throw_exception( "create needs a hashref" ) unless ref $attrs eq 'HASH'; + return $self->new_result($attrs)->insert; +} + +=head2 find_or_create + +=head3 Arguments: (\%vals, \%attrs?) + + $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. + + my $cd = $schema->resultset('CD')->find_or_create({ + cdid => 5, + artist => 'Massive Attack', + title => 'Mezzanine', + year => 2005, + }); + +Also takes an optional C attribute, to search by a specific key or unique +constraint. For example: + + my $cd = $schema->resultset('CD')->find_or_create( + { + artist => 'Massive Attack', + title => 'Mezzanine', + }, + { key => 'artist_title' } + ); + +See also L and L. + +=cut + +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); +} + +=head2 update_or_create + + $class->update_or_create({ key => $val, ... }); + +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 +row. + +Takes an optional C attribute to search on a specific unique constraint. +For example: + + # In your application + my $cd = $schema->resultset('CD')->update_or_create( + { + artist => 'Massive Attack', + title => 'Mezzanine', + year => 1998, + }, + { key => 'artist_title' } + ); + +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. + +See also L and 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); + } + + if (@unique_hashes) { + my $row = $self->single(\@unique_hashes); + if (defined $row) { + $row->set_columns($hash); + $row->update; + return $row; + } + } + + return $self->create($hash); +} + +=head2 get_cache + +Gets the contents of the cache for the resultset. + +=cut + +sub get_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. + +=cut + +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; + } + $self->{all_cache} = $data; +} + +=head2 clear_cache + +Clears the cache for the resultset. + +=cut + +sub clear_cache { + shift->set_cache([]); +} + +=head2 related_resultset + +Returns a related resultset for the supplied relationship name. + + $rs = $rs->related_resultset('foo'); + +=cut + +sub related_resultset { + my ( $self, $rel, @rest ) = @_; + $self->{related_resultsets} ||= {}; + 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); + }; +} + +=head2 throw_exception + +See Schema's throw_exception + +=cut + +sub throw_exception { + my $self=shift; + $self->result_source->schema->throw_exception(@_); } -=head1 Attributes +=head1 ATTRIBUTES -The resultset takes various attributes that modify its behavior. -Here's an overview of them: +The resultset takes various attributes that modify its behavior. Here's an +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. +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 columns + +=head3 Arguments: (arrayref) + +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. (You may also +use the C attribute, as in earlier versions of DBIC.) + +=head2 include_columns + +=head3 Arguments: (arrayref) + +Shortcut to include additional columns in the returned results - for example -=head2 cols + { include_columns => ['foo.name'], join => ['foo'] } -Which columns should be retrieved. +would add a 'name' column to the information passed to object inflation + +=head2 select + +=head3 Arguments: (arrayref) + +Indicates which columns should be selected from the storage. You can use +column names, or in the case of RDBMS back ends, function or stored procedure +names: + + $rs = $schema->resultset('Foo')->search( + undef, + { + select => [ + 'column_name', + { count => 'column_to_count' }, + { sum => 'column_to_sum' } + ] + } + ); + +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 as + +=head3 Arguments: (arrayref) + +Indicates column names for object inflation. This is used in conjunction with +C contains one or more function or stored +procedure names: + + $rs = $schema->resultset('Foo')->search( + undef, + { + select => [ + 'column1', + { count => 'column2' } + ], + as => [qw/ column1 column2_count /] + } + ); + + my $foo = $rs->first(); # get the first Foo + +If the object against which the search is performed already has an accessor +matching a column name specified in C, the value can be retrieved using +the accessor as normal: + + my $column1 = $foo->column1(); + +If on the other hand an accessor does not exist in the object, you need to +use C instead: + + my $column2_count = $foo->get_column('column2_count'); + +You can create your own accessors if required - see +L for details. =head2 join -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). -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. +Contains a list of relationships that should be joined for this query. For +example: + + # Get CDs by Nine Inch Nails + my $rs = $schema->resultset('CD')->search( + { 'artist.name' => 'Nine Inch Nails' }, + { join => 'artist' } + ); + +Can also contain a hash reference to refer to the other relation's relations. +For example: + + package MyApp::Schema::Track; + use base qw/DBIx::Class/; + __PACKAGE__->table('track'); + __PACKAGE__->add_columns(qw/trackid cd position title/); + __PACKAGE__->set_primary_key('trackid'); + __PACKAGE__->belongs_to(cd => 'MyApp::Schema::CD'); + 1; + + # In your application + my $rs = $schema->resultset('Artist')->search( + { 'track.title' => 'Teardrop' }, + { + join => { cd => 'track' }, + order_by => 'artist.name', + } + ); -=head2 prefetch +If the same join is supplied twice, it will be aliased to _2 (and +similarly for a third time). For e.g. -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. + my $rs = $schema->resultset('Artist')->search( + { 'cds.title' => 'Foo', + 'cds_2.title' => 'Bar' }, + { join => [ qw/cds cds/ ] }); -=head2 from +will return a set of all artists that have both a cd with title Foo and a cd +with title Bar. -This attribute can contain a arrayref of elements. Each element can be another -arrayref, to nest joins, or it can be a hash which represents the two sides -of the join. +If you want to fetch related objects from other tables as well, see C +below. -NOTE: Use this on your own risk. This allows you to shoot your foot off! +=head2 prefetch + +=head3 Arguments: arrayref/hashref + +Contains one or more 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 +objects, because it saves at least one query: + + my $rs = $schema->resultset('Tag')->search( + undef, + { + prefetch => { + cd => 'artist' + } + } + ); + +The initial search results in SQL like the following: + + SELECT tag.*, cd.*, artist.* FROM tag + JOIN cd ON tag.cd = cd.cdid + JOIN artist ON cd.artist = artist.artistid + +L has no need to go back to the database when we access the +C or C relationships, which saves us two SQL statements in this +case. + +Simple prefetches will be joined automatically, so there is no need +for a C attribute in the above search. If you're prefetching to +depth (e.g. { cd => { artist => 'label' } or similar), you'll need to +specify the join as well. + +C can be used with the following relationship types: C, +C (or if you're using C, any relationship declared +with an accessor type of 'single' or 'filter'). + +=head2 from + +=head3 Arguments: (arrayref) + +The C attribute gives you manual control over the C clause of SQL +statements generated by L, allowing you to express custom C +clauses. + +NOTE: Use this on your own risk. This allows you to shoot off your foot! +C will usually do what you need and it is strongly recommended that you +avoid using C unless you cannot achieve the desired result using C. + +In simple terms, C works as follows: + + [ + { => , -join-type => 'inner|left|right' } + [] # nested JOIN (optional) + { = } + ] + + JOIN +
+ [JOIN ...] + ON = + +An easy way to follow the examples below is to remember the following: + + Anything inside "[]" is a JOIN + Anything inside "{}" is a condition for the enclosing JOIN + +The following examples utilize a "person" table in a family tree application. +In order to express parent->child relationships, this table is self-joined: + + # Person->belongs_to('father' => 'Person'); + # Person->belongs_to('mother' => 'Person'); + +C can be used to nest joins. Here we return all children with a father, +then search against all mothers of those children: + + $rs = $schema->resultset('Person')->search( + undef, + { + alias => 'mother', # alias columns in accordance with "from" + from => [ + { mother => 'person' }, + [ + [ + { child => 'person' }, + [ + { father => 'person' }, + { 'father.person_id' => 'child.father_id' } + ] + ], + { 'mother.person_id' => 'child.mother_id' } + ], + ] + }, + ); + + # Equivalent SQL: + # SELECT mother.* FROM person mother + # JOIN ( + # person child + # JOIN person father + # ON ( father.person_id = child.father_id ) + # ) + # ON ( mother.person_id = child.mother_id ) + +The type of any join can be controlled manually. To search against only people +with a father in the person table, we could explicitly use C: + + $rs = $schema->resultset('Person')->search( + undef, + { + alias => 'child', # alias columns in accordance with "from" + from => [ + { child => 'person' }, + [ + { father => 'person', -join-type => 'inner' }, + { 'father.id' => 'child.father_id' } + ], + ] + }, + ); + + # Equivalent SQL: + # SELECT child.* FROM person child + # INNER JOIN person father ON child.father_id = father.id =head2 page -For a paged resultset, specifies which page to retrieve. Leave unset +For a paged resultset, specifies which page to retrieve. Leave unset for an unpaged resultset. =head2 rows -For a paged resultset, how many rows per page +For a paged resultset, how many rows per page: + + rows => 10 + +Can also be used to simulate an SQL C. + +=head2 group_by + +=head3 Arguments: (arrayref) + +A arrayref of columns to group by. Can include columns of joined tables. + + group_by => [qw/ column1 column2 ... /] + +=head2 distinct + +Set to 1 to group by all columns. + +For more examples of using these attributes, see +L. =cut