X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FResultSet.pm;h=4df3d585cc7c382048e0df46a397d48a47e51ff1;hb=4a28c3401397f015e833a802ddab5d341693f7dd;hp=881033e2ce6e67c34301a6b04e50da33d075d763;hpb=ad3d2d7c1918602f7bf2bf6fbadd97c890626d78;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/ResultSet.pm b/lib/DBIx/Class/ResultSet.pm index 881033e..4df3d58 100644 --- a/lib/DBIx/Class/ResultSet.pm +++ b/lib/DBIx/Class/ResultSet.pm @@ -2,8 +2,10 @@ package DBIx::Class::ResultSet; use strict; use warnings; +use Carp qw/croak/; use overload '0+' => 'count', + 'bool' => sub { 1; }, fallback => 1; use Data::Page; use Storable; @@ -14,22 +16,46 @@ 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__->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__->table('artist'); + __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($source, \%$attrs) -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. +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: + + my $rs = $schema->resultset('CD')->search({ title => '100th Window' }); =cut @@ -37,14 +63,15 @@ sub new { my $class = shift; return $class->new_result(@_) if ref $class; my ($source, $attrs) = @_; - #use Data::Dumper; warn Dumper(@_); + #use Data::Dumper; warn Dumper($attrs); $attrs = Storable::dclone($attrs || {}); # { %{ $attrs || {} } }; my %seen; my $alias = ($attrs->{alias} ||= 'me'); - if (!$attrs->{select}) { + if ($attrs->{cols} || !$attrs->{select}) { + delete $attrs->{as} if $attrs->{cols}; my @cols = ($attrs->{cols} ? @{delete $attrs->{cols}} - : $source->result_class->_select_columns); + : $source->columns); $attrs->{select} = [ map { m/\./ ? $_ : "${alias}.$_" } @cols ]; } $attrs->{as} ||= [ map { m/^$alias\.(.*)$/ ? $1 : $_ } @{$attrs->{select}} ]; @@ -90,13 +117,14 @@ sub new { =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({}, { cols => [qw/foo bar/] }); =cut @@ -125,12 +153,13 @@ sub search { 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 @@ -141,25 +170,48 @@ sub search_literal { return $self->search(\$cond, $attrs); } -=head2 find(@colvalues), find(\%cols) +=head2 find(@colvalues), find(\%cols, \%attrs?) + +Finds a row based on its primary key or unique constraint. For example: + + my $cd = $schema->resultset('CD')->find(5); -Finds a row based on its primary key(s). +Also takes an optional C attribute, to search by a specific key or unique +constraint. For example: -=cut + my $cd = $schema->resultset('CD')->find_or_create( + { + 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 @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 @cols = $self->{source}->primary_columns; + if (exists $attrs->{key}) { + my %uniq = $self->{source}->unique_constraints; + $self->( "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->{source}->result_class->throw( "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 (@pk == @vals) { + } elsif (@cols == @vals) { $query = {}; - @{$query}{@pk} = @vals; + @{$query}{@cols} = @vals; } else { $query = {@vals}; } @@ -175,6 +227,9 @@ sub find { $rs->search_related('relname', $cond?, $attrs?); +Search the specified relationship. Optionally specify a condition for matching +records. + =cut sub search_related { @@ -208,11 +263,15 @@ sub cursor { $attrs->{where},$attrs); } -=head2 search_like - -Identical to search except defaults to 'LIKE' instead of '=' in condition - -=cut +=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; @@ -241,15 +300,23 @@ sub slice { 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; +# warn Dumper(\@row); use Data::Dumper; return unless (@row); return $self->_construct_object(@row); } @@ -266,7 +333,8 @@ sub _construct_object { $me{$col} = shift @row; } } - my $new = $self->{source}->result_class->inflate_result(\%me, \%pre); + my $new = $self->{source}->result_class->inflate_result( + $self->{source}, \%me, \%pre); $new = $self->{attrs}{record_filter}->($new) if exists $self->{attrs}{record_filter}; return $new; @@ -283,7 +351,7 @@ on the resultset and counts the results of that. sub count { my $self = shift; return $self->search(@_)->count if @_ && defined $_[0]; - die "Unable to ->count with a GROUP BY" if defined $self->{attrs}{group_by}; + croak "Unable to ->count with a GROUP BY" if defined $self->{attrs}{group_by}; unless (defined $self->{count}) { my $attrs = { %{ $self->{attrs} }, select => { 'count' => '*' }, @@ -303,7 +371,7 @@ sub count { =head2 count_literal -Calls search_literal with the passed arguments, then count. +Calls L with the passed arguments, then L. =cut @@ -344,31 +412,71 @@ sub first { return $_[0]->reset->next; } +=head2 update(\%values) + +Sets the specified columns in the resultset to the supplied values. + +=cut + +sub update { + my ($self, $values) = @_; + croak "Values for update must be a hash" unless ref $values eq 'HASH'; + return $self->{source}->storage->update( + $self->{source}->from, $values, $self->{cond}); +} + +=head2 update_all(\%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) = @_; + croak "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; + $self->{source}->storage->delete($self->{source}->from, $self->{cond}); 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}; - die "Can't create pager for non-paged rs" unless $self->{page}; + croak "Can't create pager for non-paged rs" unless $self->{page}; $attrs->{rows} ||= 10; $self->count; return $self->{pager} ||= Data::Page->new( @@ -390,7 +498,7 @@ sub page { =head2 new_result(\%vals) -Creates a result in the resultset's result class +Creates a result in the resultset's result class. =cut @@ -405,14 +513,16 @@ sub new_result { foreach my $key (keys %{$self->{cond}||{}}) { $new{$1} = $self->{cond}{$key} if ($key =~ m/^(?:$alias\.)?([^\.]+)$/); } - return $self->{source}->result_class->new(\%new); + my $obj = $self->{source}->result_class->new(\%new); + $obj->result_source($self->{source}) if $obj->can('result_source'); + $obj; } =head2 create(\%vals) -Inserts a record into the resultset and returns the object +Inserts a record into the resultset and returns the object. -Effectively a shortcut for ->new_result(\%vals)->insert +Effectively a shortcut for C<< ->new_result(\%vals)->insert >>. =cut @@ -422,90 +532,338 @@ sub create { return $self->new_result($attrs)->insert; } -=head2 find_or_create(\%vals) +=head2 find_or_create(\%vals, \%attrs?) + + $class->find_or_create({ key => $val, ... }); - $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 $hash = ref $_[0] eq "HASH" ? shift: {@_}; - my $exists = $self->find($hash); + 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->{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; + } + } + + unless ($row) { + $row = $self->create($hash); + } + + return $row; +} + =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 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 +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. =head2 select (arrayref) -Indicates which columns should be selected from the storage +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( + {}, + { + 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 (arrayref) -Indicates column names for object inflation +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( + {}, + { + 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', + } + ); + +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 +"prefetched"). This is useful for when you know you will need the related +objects, 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 -arrayref, to nest joins, or it can be a hash which represents the two sides -of the join. - -NOTE: Use this on your own risk. This allows you to shoot your foot off! +=head2 from (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( + {}, + { + 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( + {}, + { + 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 -=head2 group_by +Can also be used to simulate an SQL C. -A list of columns to group by (note that 'count' doesn't work on grouped -resultsets) +=head2 group_by (arrayref) + +A arrayref of columns to group by. Can include columns of joined tables. Note +note that L doesn't work on grouped resultsets. + + group_by => [qw/ column1 column2 ... /] =head2 distinct -Set to 1 to group by all columns +Set to 1 to group by all columns. + +For more examples of using these attributes, see +L. =cut