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
=head3 Arguments: ($source, \%$attrs)
The resultset constructor. Takes a source object (usually a
-L<DBIx::Class::ResultSourceProxy::Table>) and an attribute hash (see L</ATRRIBUTES>
+L<DBIx::Class::ResultSourceProxy::Table>) and an attribute hash (see L</ATTRIBUTES>
below). Does not perform any queries -- these are executed as needed by the
other methods.
sub new {
my $class = shift;
return $class->new_result(@_) if ref $class;
+
my ($source, $attrs) = @_;
#use Data::Dumper; warn Dumper($attrs);
$attrs = Storable::dclone($attrs || {}); # { %{ $attrs || {} } };
- my %seen;
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->{columns} ||= delete $attrs->{cols} if $attrs->{cols};
+ $attrs->{columns} ||= [ $source->columns ] unless $attrs->{select};
+ if ($attrs->{columns}) {
+ delete $attrs->{as};
+ $attrs->{select} = [ map { m/\./ ? $_ : "${alias}.$_" } @{delete $attrs->{columns}} ];
}
- $attrs->{as} ||= [ map { m/^$alias\.(.*)$/ ? $1 : $_ } @{$attrs->{select}} ];
+ $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;
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 {
}
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 !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 $new_rs = $rs->search({ foo => 3 });
If you need to pass in additional attributes but no additional condition,
-call it as C<search({}, \%attrs);>.
+call it as C<search(undef, \%attrs);>.
# "SELECT foo, bar FROM $class_table"
- my @all = $class->search({}, { cols => [qw/foo bar/] });
+ my @all = $class->search(undef, { columns => [qw/foo bar/] });
=cut
$query->{$self->{attrs}{alias}.'.'.$_} = delete $query->{$_};
}
#warn Dumper($query);
- return (keys %$attrs
- ? $self->search($query,$attrs)->single
- : $self->single($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
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;
}
return $obj;
}
if ($self->{attrs}{cache}) {
- $self->{all_cache_position} = 0;
+ $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);
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} };
-#use Data::Dumper; warn Dumper \@as;
- #warn "@cols -> @row";
- my $info = [ {}, {} ];
- foreach my $as (@as) {
- my $rs = $self;
- my $target = $info;
- my @parts = split(/\./, $as);
- my $col = pop(@parts);
- foreach my $p (@parts) {
- $target = $target->[1]->{$p} ||= [];
-
- $rs = $rs->related_resultset($p) if $rs->{attrs}->{cache};
- }
-
- $target->[0]->{$col} = shift @row
- if ref($target->[0]) ne 'ARRAY'; # arrayref is pre-inflated objects, do not overwrite
- }
- #use Data::Dumper; warn Dumper(\@as, $info);
- my $new = $self->result_source->result_class->inflate_result(
- $self->result_source, @$info);
+
+ 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};
- if( $self->{attrs}->{cache} ) {
- while( my( $rel, $rs ) = each( %{$self->{related_resultsets}} ) ) {
- $rs->all;
- #warn "$rel:", @{$rs->get_cache};
- }
- $self->build_rr( $self, $new );
- }
-
return $new;
}
-
-sub build_rr {
- # build related resultsets for supplied object
- my ( $self, $context, $obj ) = @_;
-
- my $re = qr/^\w+\./;
- while( my ($rel, $rs) = each( %{$context->{related_resultsets}} ) ) {
- #warn "context:", $context->result_source->name, ", rel:$rel, rs:", $rs->result_source->name;
- my @objs = ();
- my $map = {};
- my $cond = $context->result_source->relationship_info($rel)->{cond};
- keys %$cond;
- while( my( $rel_key, $pk ) = each(%$cond) ) {
- $rel_key =~ s/$re//;
- $pk =~ s/$re//;
- $map->{$rel_key} = $pk;
+
+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;
}
-
- $rs->reset();
- while( my $rel_obj = $rs->next ) {
- while( my( $rel_key, $pk ) = each(%$map) ) {
- if( $rel_obj->get_column($rel_key) eq $obj->get_column($pk) ) {
- push @objs, $rel_obj;
- }
+ }
+
+ 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 $rel_rs = $obj->related_resultset($rel);
- $rel_rs->{attrs}->{cache} = 1;
- $rel_rs->set_cache( \@objs );
-
- while( my $rel_obj = $rel_rs->next ) {
- $self->build_rr( $rs, $rel_obj );
+ 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
my ($self) = @_;
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
+ my @row;
+ $self->cursor->reset;
+ while (@row = $self->cursor->next) {
+ push(@obj, $self->_construct_object(@row));
+ }
+ } else {
+ @obj = map { $self->_construct_object(@$_); }
+ $self->cursor->all;
+ }
+
if( $self->{attrs}->{cache} ) {
- my @obj = map { $self->_construct_object(@$_); }
- $self->cursor->all;
$self->set_cache( \@obj );
- return @obj;
}
- return map { $self->_construct_object(@$_); }
- $self->cursor->all;
+
+ return @obj;
}
=head2 reset
foreach my $key (keys %{$self->{cond}||{}}) {
$new{$1} = $self->{cond}{$key} if ($key =~ m/^(?:$alias\.)?([^\.]+)$/);
}
- 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;
}
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;
"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 });
- }
+ my $rs = $self->search(undef, { join => $rel });
+ #if( $self->{attrs}->{cache} ) {
+ # $rs = $self->search(undef);
+ #}
+ #else {
+ #}
#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}
Which column(s) to order the results by. This is currently passed through
directly to SQL, so you can give e.g. C<foo DESC> for a descending order.
-=head2 cols
+=head2 columns
=head3 Arguments: (arrayref)
Shortcut to request a particular set of columns to be retrieved. Adds
C<me.> onto the start of any column without a C<.> in it and sets C<select>
-from that, then auto-populates C<as> from C<select> as normal.
+from that, then auto-populates C<as> from C<select> as normal. (You may also
+use the C<cols> attribute, as in earlier versions of DBIC.)
=head2 include_columns
names:
$rs = $schema->resultset('Foo')->search(
- {},
+ undef,
{
select => [
'column_name',
procedure names:
$rs = $schema->resultset('Foo')->search(
- {},
+ undef,
{
select => [
'column1',
objects, because it saves at least one query:
my $rs = $schema->resultset('Tag')->search(
- {},
+ undef,
{
prefetch => {
cd => 'artist'
then search against all mothers of those children:
$rs = $schema->resultset('Person')->search(
- {},
+ undef,
{
alias => 'mother', # alias columns in accordance with "from"
from => [
with a father in the person table, we could explicitly use C<INNER JOIN>:
$rs = $schema->resultset('Person')->search(
- {},
+ undef,
{
alias => 'child', # alias columns in accordance with "from"
from => [