X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FResultSource.pm;h=4a6eaa8ad96522f3d47b1d6667ad32bccdd8a6a1;hb=d40080c39809e75e0aa8b949ea157e274db1b66d;hp=41d55622c641e27544f33feed32a4384987144cb;hpb=391ccf38fb53560c1eee1012fc3e483f42fe607b;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/ResultSource.pm b/lib/DBIx/Class/ResultSource.pm index 41d5562..4a6eaa8 100644 --- a/lib/DBIx/Class/ResultSource.pm +++ b/lib/DBIx/Class/ResultSource.pm @@ -13,7 +13,7 @@ use base qw/DBIx::Class/; __PACKAGE__->mk_group_accessors('simple' => qw/_ordered_columns _columns _primaries _unique_constraints name resultset_attributes schema from _relationships column_info_from_storage source_info - source_name/); + source_name sqlt_deploy_callback/); __PACKAGE__->mk_group_accessors('component_class' => qw/resultset_class result_class/); @@ -29,18 +29,12 @@ DBIx::Class::ResultSource - Result source object A ResultSource is a component of a schema from which results can be directly retrieved, most usually a table (see L) +Basic view support also exists, see L<. + =head1 METHODS =pod -=head2 new - - $class->new(); - - $class->new({attribute_name => value}); - -Creates a new ResultSource object. Not normally called directly by end users. - =cut sub new { @@ -55,22 +49,12 @@ sub new { $new->{_relationships} = { %{$new->{_relationships}||{}} }; $new->{name} ||= "!!NAME NOT SET!!"; $new->{_columns_info_loaded} ||= 0; + $new->{sqlt_deploy_callback} ||= "default_sqlt_deploy_hook"; return $new; } =pod -=head2 source_info - -Stores a hashref of per-source metadata. No specific key names -have yet been standardized, the examples below are purely hypothetical -and don't actually accomplish anything on their own: - - __PACKAGE__->source_info({ - "_tablespace" => 'fast_disk_array_3', - "_engine" => 'InnoDB', - }); - =head2 add_columns =over @@ -81,16 +65,16 @@ and don't actually accomplish anything on their own: =back - $table->add_columns(qw/col1 col2 col3/); + $source->add_columns(qw/col1 col2 col3/); - $table->add_columns('col1' => \%col1_info, 'col2' => \%col2_info, ...); + $source->add_columns('col1' => \%col1_info, 'col2' => \%col2_info, ...); Adds columns to the result source. If supplied key => hashref pairs, uses the hashref as the column_info for that column. Repeated calls of this method will add more columns, not replace them. The column names given will be created as accessor methods on your -L objects, you can change the name of the accessor +L objects. You can change the name of the accessor by supplying an L in the column_info hash. The contents of the column_info are not set in stone. The following @@ -141,8 +125,12 @@ L. =item default_value Set this to the default value which will be inserted into a column -by the database. Can contain either a value or a function. This is -currently only used by L. +by the database. Can contain either a value or a function (use a +reference to a scalar e.g. C<\'now()'> if you want a function). This +is currently only used by L. + +See the note on L for more information about possible +issues related to db-side default values. =item sequence @@ -154,7 +142,7 @@ automatically. =item auto_nextval Set this to a true value for a column whose value is retrieved -automatically from an oracle sequence. If you do not use an oracle +automatically from an oracle sequence. If you do not use an Oracle trigger to get the nextval, you have to set sequence as well. =item extra @@ -177,7 +165,7 @@ L. =back - $table->add_column('col' => \%info?); + $source->add_column('col' => \%info?); Add a single column and optional column info. Uses the same column info keys as L. @@ -213,7 +201,7 @@ sub add_column { shift->add_columns(@_); } # DO NOT CHANGE THIS TO GLOB =back - if ($obj->has_column($colname)) { ... } + if ($source->has_column($colname)) { ... } Returns true if the source has a column of this name, false otherwise. @@ -234,7 +222,7 @@ sub has_column { =back - my $info = $obj->column_info($col); + my $info = $source->column_info($col); Returns the column metadata hashref for a column, as originally passed to L. See the description of L for information @@ -272,22 +260,6 @@ sub column_info { return $self->_columns->{$column}; } -=head2 column_info_from_storage - -=over - -=item Arguments: 1/0 (default: 0) - -=item Return value: 1/0 - -=back - -Enables the on-demand automatic loading of the above column -metadata from storage as neccesary. This is *deprecated*, and -should not be used. It will be removed before 1.0. - - __PACKAGE__->column_info_from_storage(1); - =head2 columns =over @@ -352,22 +324,18 @@ broken result source. =cut sub remove_columns { - my ($self, @cols) = @_; + my ($self, @to_remove) = @_; - return unless $self->_ordered_columns; + my $columns = $self->_columns + or return; - my $columns = $self->_columns; - my @remaining; - - foreach my $col (@{$self->_ordered_columns}) { - push @remaining, $col unless grep(/$col/, @cols); - } - - foreach (@cols) { + my %to_remove; + for (@to_remove) { delete $columns->{$_}; - }; + ++$to_remove{$_}; + } - $self->_ordered_columns(\@remaining); + $self->_ordered_columns([ grep { not $to_remove{$_} } @{$self->_ordered_columns} ]); } sub remove_column { shift->remove_columns(@_); } # DO NOT CHANGE THIS TO GLOB @@ -582,6 +550,171 @@ sub unique_constraint_columns { return @{ $unique_constraints{$constraint_name} }; } +=head2 sqlt_deploy_callback + +=over + +=item Arguments: $callback + +=back + + __PACKAGE__->sqlt_deploy_callback('mycallbackmethod'); + +An accessor to set a callback to be called during deployment of +the schema via L or +L. + +The callback can be set as either a code reference or the name of a +method in the current result class. + +If not set, the L is called. + +Your callback will be passed the $source object representing the +ResultSource instance being deployed, and the +L object being created from it. The +callback can be used to manipulate the table object or add your own +customised indexes. If you need to manipulate a non-table object, use +the L. + +See L for examples. + +This sqlt deployment callback can only be used to manipulate +SQL::Translator objects as they get turned into SQL. To execute +post-deploy statements which SQL::Translator does not currently +handle, override L in your Schema class +and call L. + +=head2 default_sqlt_deploy_hook + +=over + +=item Arguments: $source, $sqlt_table + +=item Return value: undefined + +=back + +This is the sensible default for L. + +If a method named C exists in your Result class, it +will be called and passed the current C<$source> and the +C<$sqlt_table> being deployed. + +=cut + +sub default_sqlt_deploy_hook { + my $self = shift; + + my $class = $self->result_class; + + if ($class and $class->can('sqlt_deploy_hook')) { + $class->sqlt_deploy_hook(@_); + } +} + +sub _invoke_sqlt_deploy_hook { + my $self = shift; + if ( my $hook = $self->sqlt_deploy_callback) { + $self->$hook(@_); + } +} + +=head2 resultset + +=over 4 + +=item Arguments: None + +=item Return value: $resultset + +=back + +Returns a resultset for the given source. This will initially be created +on demand by calling + + $self->resultset_class->new($self, $self->resultset_attributes) + +but is cached from then on unless resultset_class changes. + +=head2 resultset_class + +=over 4 + +=item Arguments: $classname + +=item Return value: $classname + +=back + + package My::ResultSetClass; + use base 'DBIx::Class::ResultSet'; + ... + + $source->resultset_class('My::ResultSet::Class'); + +Set the class of the resultset. This is useful if you want to create your +own resultset methods. Create your own class derived from +L, and set it here. If called with no arguments, +this method returns the name of the existing resultset class, if one +exists. + +=head2 resultset_attributes + +=over 4 + +=item Arguments: \%attrs + +=item Return value: \%attrs + +=back + + $source->resultset_attributes({ order_by => [ 'id' ] }); + +Store a collection of resultset attributes, that will be set on every +L produced from this result source. For a full +list see L. + +=cut + +sub resultset { + my $self = shift; + $self->throw_exception( + 'resultset does not take any arguments. If you want another resultset, '. + 'call it on the schema instead.' + ) if scalar @_; + + return $self->resultset_class->new( + $self, + { + %{$self->{resultset_attributes}}, + %{$self->schema->default_resultset_attributes} + }, + ); +} + +=head2 source_name + +=over 4 + +=item Arguments: $source_name + +=item Result value: $source_name + +=back + +Set an alternate name for the result source when it is loaded into a schema. +This is useful if you want to refer to a result source by a name other than +its class name. + + package ArchivedBooks; + use base qw/DBIx::Class/; + __PACKAGE__->table('books_archive'); + __PACKAGE__->source_name('Books'); + + # from your schema... + $schema->resultset('Books')->find(1); + =head2 from =over 4 @@ -708,7 +841,7 @@ relationship. =back Throws an exception if the condition is improperly supplied, or cannot -be resolved using L. +be resolved. =cut @@ -748,7 +881,7 @@ sub add_relationship { } return unless $f_source; # Can't test rel without f_source - eval { $self->resolve_join($rel, 'me') }; + eval { $self->_resolve_join($rel, 'me') }; if ($@) { # If the resolve failed, back out and re-throw the error delete $rels{$rel}; # @@ -882,29 +1015,22 @@ sub reverse_relationship_info { my @other_cond = keys(%$othercond); my @other_refkeys = map {/^\w+\.(\w+)$/} @other_cond; my @other_keys = map {$othercond->{$_} =~ /^\w+\.(\w+)$/} @other_cond; - next if (!$self->compare_relationship_keys(\@refkeys, \@other_keys) || - !$self->compare_relationship_keys(\@other_refkeys, \@keys)); + next if (!$self->_compare_relationship_keys(\@refkeys, \@other_keys) || + !$self->_compare_relationship_keys(\@other_refkeys, \@keys)); $ret->{$otherrel} = $otherrel_info; } } return $ret; } -=head2 compare_relationship_keys - -=over 4 - -=item Arguments: \@keys1, \@keys2 - -=item Return value: 1/0 (true/false) - -=back - -Returns true if both sets of keynames are the same, false otherwise. - -=cut - sub compare_relationship_keys { + carp 'compare_relationship_keys is a private method, stop calling it'; + my $self = shift; + $self->_compare_relationship_keys (@_); +} + +# Returns true if both sets of keynames are the same, false otherwise. +sub _compare_relationship_keys { my ($self, $keys1, $keys2) = @_; # Make sure every keys1 is in keys2 @@ -937,44 +1063,54 @@ sub compare_relationship_keys { return $found; } -=head2 resolve_join - -=over 4 +sub resolve_join { + carp 'resolve_join is a private method, stop calling it'; + my $self = shift; + $self->_resolve_join (@_); +} -=item Arguments: $relation +# Returns the {from} structure used to express JOIN conditions +sub _resolve_join { + my ($self, $join, $alias, $seen, $force_left, $jpath) = @_; -=item Return value: Join condition arrayref + # we need a supplied one, because we do in-place modifications, no returns + $self->throw_exception ('You must supply a seen hashref as the 3rd argument to _resolve_join') + unless $seen; -=back + $force_left ||= { force => 0 }; -Returns the join structure required for the related result source. + # This isn't quite right, we should actually dive into $seen and reconstruct + # the entire path (the reference entry point would be the join conditional + # with depth == current_depth - 1. At this point however nothing depends on + # having the entire path, transcending related_resultset, so just leave it + # as is, hairy enough already. + $jpath ||= []; -=cut - -sub resolve_join { - my ($self, $join, $alias, $seen, $force_left) = @_; - $seen ||= {}; - $force_left ||= { force => 0 }; if (ref $join eq 'ARRAY') { - return map { $self->resolve_join($_, $alias, $seen) } @$join; + return + map { + local $force_left->{force} = $force_left->{force}; + $self->_resolve_join($_, $alias, $seen, $force_left, [@$jpath]); + } @$join; } elsif (ref $join eq 'HASH') { return map { - my $as = ($seen->{$_} ? $_.'_'.($seen->{$_}+1) : $_); - local $force_left->{force}; + my $as = ($seen->{$_} ? join ('_', $_, $seen->{$_} + 1) : $_); # the actual seen value will be incremented below + local $force_left->{force} = $force_left->{force}; ( - $self->resolve_join($_, $alias, $seen, $force_left), - $self->related_source($_)->resolve_join( - $join->{$_}, $as, $seen, $force_left + $self->_resolve_join($_, $alias, $seen, $force_left, [@$jpath]), + $self->related_source($_)->_resolve_join( + $join->{$_}, $as, $seen, $force_left, [@$jpath, $_] ) ); } keys %$join; } elsif (ref $join) { $self->throw_exception("No idea how to resolve join reftype ".ref $join); } else { + my $count = ++$seen->{$join}; - #use Data::Dumper; warn Dumper($seen); my $as = ($count > 1 ? "${join}_${count}" : $join); + my $rel_info = $self->relationship_info($join); $self->throw_exception("No such relationship ${join}") unless $rel_info; my $type; @@ -985,28 +1121,25 @@ sub resolve_join { $force_left->{force} = 1 if lc($type) eq 'left'; } return [ { $as => $self->related_source($join)->from, - -join_type => $type }, - $self->resolve_condition($rel_info->{cond}, $as, $alias) ]; + -join_type => $type, + -join_path => [@$jpath, $join], + -join_alias => $as, + -relation_chain_depth => $seen->{-relation_chain_depth} || 0, + }, + $self->_resolve_condition($rel_info->{cond}, $as, $alias) ]; } } -=head2 pk_depends_on - -=over 4 - -=item Arguments: $relname, $rel_data - -=item Return value: 1/0 (true/false) - -=back - -Determines whether a relation is dependent on an object from this source -having already been inserted. Takes the name of the relationship and a -hashref of columns of the related object. - -=cut - sub pk_depends_on { + carp 'pk_depends_on is a private method, stop calling it'; + my $self = shift; + $self->_pk_depends_on (@_); +} + +# Determines whether a relation is dependent on an object from this source +# having already been inserted. Takes the name of the relationship and a +# hashref of columns of the related object. +sub _pk_depends_on { my ($self, $relname, $rel_data) = @_; my $cond = $self->relationship_info($relname)->{cond}; @@ -1035,23 +1168,18 @@ sub pk_depends_on { return 1; } -=head2 resolve_condition - -=over 4 - -=item Arguments: $cond, $as, $alias|$object - -=back - -Resolves the passed condition to a concrete query fragment. If given an alias, -returns a join condition; if given an object, inverts that object to produce -a related conditional from that object. - -=cut +sub resolve_condition { + carp 'resolve_condition is a private method, stop calling it'; + my $self = shift; + $self->_resolve_condition (@_); +} +# Resolves the passed condition to a concrete query fragment. If given an alias, +# returns a join condition; if given an object, inverts that object to produce +# a related conditional from that object. our $UNRESOLVABLE_CONDITION = \'1 = 0'; -sub resolve_condition { +sub _resolve_condition { my ($self, $cond, $as, $for) = @_; #warn %$cond; if (ref $cond eq 'HASH') { @@ -1067,7 +1195,11 @@ sub resolve_condition { #warn "$self $k $for $v"; unless ($for->has_column_loaded($v)) { if ($for->in_storage) { - $self->throw_exception("Column ${v} not loaded on ${for} trying to resolve relationship"); + $self->throw_exception( + "Column ${v} not loaded or not passed to new() prior to insert()" + ." on ${for} trying to resolve relationship (maybe you forgot " + ."to call ->reload_from_storage to get defaults from the db)" + ); } return $UNRESOLVABLE_CONDITION; } @@ -1088,66 +1220,18 @@ sub resolve_condition { } return \%ret; } elsif (ref $cond eq 'ARRAY') { - return [ map { $self->resolve_condition($_, $as, $for) } @$cond ]; + return [ map { $self->_resolve_condition($_, $as, $for) } @$cond ]; } else { die("Can't handle this yet :("); } } -=head2 resolve_prefetch - -=over 4 - -=item Arguments: hashref/arrayref/scalar - -=back - -Accepts one or more relationships for the current source and returns an -array of column names for each of those relationships. Column names are -prefixed relative to the current source, in accordance with where they appear -in the supplied relationships. Examples: - - my $source = $schema->resultset('Tag')->source; - @columns = $source->resolve_prefetch( { cd => 'artist' } ); - - # @columns = - #( - # 'cd.cdid', - # 'cd.artist', - # 'cd.title', - # 'cd.year', - # 'cd.artist.artistid', - # 'cd.artist.name' - #) - - @columns = $source->resolve_prefetch( qw[/ cd /] ); - - # @columns = - #( - # 'cd.cdid', - # 'cd.artist', - # 'cd.title', - # 'cd.year' - #) - - $source = $schema->resultset('CD')->source; - @columns = $source->resolve_prefetch( qw[/ artist producer /] ); - - # @columns = - #( - # 'artist.artistid', - # 'artist.name', - # 'producer.producerid', - # 'producer.name' - #) - -=cut - +# Legacy code, needs to go entirely away (fully replaced by _resolve_prefetch) sub resolve_prefetch { + carp 'resolve_prefetch is a private method, stop calling it'; + my ($self, $pre, $alias, $seen, $order, $collapse) = @_; $seen ||= {}; - #$alias ||= $self->name; - #warn $alias, Dumper $pre; if( ref $pre eq 'ARRAY' ) { return map { $self->resolve_prefetch( $_, $alias, $seen, $order, $collapse ) } @@ -1160,7 +1244,6 @@ sub resolve_prefetch { $self->related_source($_)->resolve_prefetch( $pre->{$_}, "${alias}.$_", $seen, $order, $collapse) } keys %$pre; - #die Dumper \@ret; return @ret; } elsif( ref $pre ) { @@ -1213,8 +1296,92 @@ sub resolve_prefetch { return map { [ "${as}.$_", "${as_prefix}${pre}.$_", ] } $rel_source->columns; - #warn $alias, Dumper (\@ret); - #return @ret; + } +} + +# Accepts one or more relationships for the current source and returns an +# array of column names for each of those relationships. Column names are +# prefixed relative to the current source, in accordance with where they appear +# in the supplied relationships. Needs an alias_map generated by +# $rs->_joinpath_aliases + +sub _resolve_prefetch { + my ($self, $pre, $alias, $alias_map, $order, $collapse, $pref_path) = @_; + $pref_path ||= []; + + if( ref $pre eq 'ARRAY' ) { + return + map { $self->_resolve_prefetch( $_, $alias, $alias_map, $order, $collapse, [ @$pref_path ] ) } + @$pre; + } + elsif( ref $pre eq 'HASH' ) { + my @ret = + map { + $self->_resolve_prefetch($_, $alias, $alias_map, $order, $collapse, [ @$pref_path ] ), + $self->related_source($_)->_resolve_prefetch( + $pre->{$_}, "${alias}.$_", $alias_map, $order, $collapse, [ @$pref_path, $_] ) + } keys %$pre; + return @ret; + } + elsif( ref $pre ) { + $self->throw_exception( + "don't know how to resolve prefetch reftype ".ref($pre)); + } + else { + + my $p = $alias_map; + $p = $p->{$_} for (@$pref_path, $pre); + + $self->throw_exception ( + "Unable to resolve prefetch $pre - join alias map does not contain an entry for path " + . join (' -> ', @$pref_path, $pre) + ) if (ref $p->{-join_aliases} ne 'ARRAY' or not @{$p->{-join_aliases}} ); + + my $as = shift @{$p->{-join_aliases}}; + + my $rel_info = $self->relationship_info( $pre ); + $self->throw_exception( $self->name . " has no such relationship '$pre'" ) + unless $rel_info; + my $as_prefix = ($alias =~ /^.*?\.(.+)$/ ? $1.'.' : ''); + my $rel_source = $self->related_source($pre); + + if (exists $rel_info->{attrs}{accessor} + && $rel_info->{attrs}{accessor} eq 'multi') { + $self->throw_exception( + "Can't prefetch has_many ${pre} (join cond too complex)") + unless ref($rel_info->{cond}) eq 'HASH'; + my $dots = @{[$as_prefix =~ m/\./g]} + 1; # +1 to match the ".${as_prefix}" + if (my ($fail) = grep { @{[$_ =~ m/\./g]} == $dots } + keys %{$collapse}) { + my ($last) = ($fail =~ /([^\.]+)$/); + carp ( + "Prefetching multiple has_many rels ${last} and ${pre} " + .(length($as_prefix) + ? "at the same level (${as_prefix}) " + : "at top level " + ) + . 'will currently disrupt both the functionality of $rs->count(), ' + . 'and the amount of objects retrievable via $rs->next(). ' + . 'Use at your own risk.' + ); + } + #my @col = map { (/^self\.(.+)$/ ? ("${as_prefix}.$1") : ()); } + # values %{$rel_info->{cond}}; + $collapse->{".${as_prefix}${pre}"} = [ $rel_source->primary_columns ]; + # action at a distance. prepending the '.' allows simpler code + # in ResultSet->_collapse_result + my @key = map { (/^foreign\.(.+)$/ ? ($1) : ()); } + keys %{$rel_info->{cond}}; + my @ord = (ref($rel_info->{attrs}{order_by}) eq 'ARRAY' + ? @{$rel_info->{attrs}{order_by}} + : (defined $rel_info->{attrs}{order_by} + ? ($rel_info->{attrs}{order_by}) + : ())); + push(@$order, map { "${as}.$_" } (@key, @ord)); + } + + return map { [ "${as}.$_", "${as_prefix}${pre}.$_", ] } + $rel_source->columns; } } @@ -1262,101 +1429,6 @@ sub related_class { return $self->schema->class($self->relationship_info($rel)->{source}); } -=head2 resultset - -=over 4 - -=item Arguments: None - -=item Return value: $resultset - -=back - -Returns a resultset for the given source. This will initially be created -on demand by calling - - $self->resultset_class->new($self, $self->resultset_attributes) - -but is cached from then on unless resultset_class changes. - -=head2 resultset_class - -=over 4 - -=item Arguments: $classname - -=item Return value: $classname - -=back - - package My::ResultSetClass; - use base 'DBIx::Class::ResultSet'; - ... - - $source->resultset_class('My::ResultSet::Class'); - -Set the class of the resultset, this is useful if you want to create your -own resultset methods. Create your own class derived from -L, and set it here. If called with no arguments, -this method returns the name of the existing resultset class, if one -exists. - -=head2 resultset_attributes - -=over 4 - -=item Arguments: \%attrs - -=item Return value: \%attrs - -=back - - $source->resultset_attributes({ order_by => [ 'id' ] }); - -Store a collection of resultset attributes, that will be set on every -L produced from this result source. For a full -list see L. - -=cut - -sub resultset { - my $self = shift; - $self->throw_exception( - 'resultset does not take any arguments. If you want another resultset, '. - 'call it on the schema instead.' - ) if scalar @_; - - return $self->resultset_class->new( - $self, - { - %{$self->{resultset_attributes}}, - %{$self->schema->default_resultset_attributes} - }, - ); -} - -=head2 source_name - -=over 4 - -=item Arguments: $source_name - -=item Result value: $source_name - -=back - -Set an alternate name for the result source when it is loaded into a schema. -This is useful if you want to refer to a result source by a name other than -its class name. - - package ArchivedBooks; - use base qw/DBIx::Class/; - __PACKAGE__->table('books_archive'); - __PACKAGE__->source_name('Books'); - - # from your schema... - $schema->resultset('Books')->find(1); - =head2 handle Obtain a new handle to this source. Returns an instance of a @@ -1386,25 +1458,41 @@ sub throw_exception { } } -=head2 sqlt_deploy_hook($sqlt_table) +=head2 source_info -=over 4 +Stores a hashref of per-source metadata. No specific key names +have yet been standardized, the examples below are purely hypothetical +and don't actually accomplish anything on their own: -=item Arguments: $source, $sqlt_table + __PACKAGE__->source_info({ + "_tablespace" => 'fast_disk_array_3', + "_engine" => 'InnoDB', + }); -=item Return value: undefined +=head2 new + + $class->new(); + + $class->new({attribute_name => value}); + +Creates a new ResultSource object. Not normally called directly by end users. + +=head2 column_info_from_storage + +=over + +=item Arguments: 1/0 (default: 0) + +=item Return value: 1/0 =back -An optional sub which you can declare in your own Result class that will get -passed the L object when you deploy the schema -via L or L. + __PACKAGE__->column_info_from_storage(1); -This is useful to make L create non-unique indexes, -or set table options such as C. +Enables the on-demand automatic loading of the above column +metadata from storage as neccesary. This is *deprecated*, and +should not be used. It will be removed before 1.0. -For an example of what you can do with this, see -L. =head1 AUTHORS