X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FResultSource.pm;h=1da92f4b781183507400477919f9c43a866f23cb;hb=c13002976e32b818eabc3a8eaf6fa2e23ebed7e9;hp=e3c2b80cfaa01024ca10a480a8b67bb113bf6237;hpb=e9188247f020a63ab8b6280c9dcdcb0df5b5f0c1;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/ResultSource.pm b/lib/DBIx/Class/ResultSource.pm index e3c2b80..1da92f4 100644 --- a/lib/DBIx/Class/ResultSource.pm +++ b/lib/DBIx/Class/ResultSource.pm @@ -5,42 +5,97 @@ use warnings; use DBIx::Class::ResultSet; use DBIx::Class::ResultSourceHandle; + +use DBIx::Class::Exception; use Carp::Clan qw/^DBIx::Class/; -use Storable; 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/); + schema from _relationships column_info_from_storage source_info + source_name sqlt_deploy_callback/); -__PACKAGE__->mk_group_accessors('inherited' => qw/resultset_class +__PACKAGE__->mk_group_accessors('component_class' => qw/resultset_class result_class/); -__PACKAGE__->mk_group_ro_accessors('simple' => qw/source_name/); - =head1 NAME DBIx::Class::ResultSource - Result source object =head1 SYNOPSIS + # Create a table based result source, in a result class. + + package MyDB::Schema::Result::Artist; + use base qw/DBIx::Class::Core/; + + __PACKAGE__->table('artist'); + __PACKAGE__->add_columns(qw/ artistid name /); + __PACKAGE__->set_primary_key('artistid'); + __PACKAGE__->has_many(cds => 'MyDB::Schema::Result::CD'); + + 1; + + # Create a query (view) based result source, in a result class + package MyDB::Schema::Result::Year2000CDs; + use base qw/DBIx::Class::Core/; + + __PACKAGE__->load_components('InflateColumn::DateTime'); + __PACKAGE__->table_class('DBIx::Class::ResultSource::View'); + + __PACKAGE__->table('year2000cds'); + __PACKAGE__->result_source_instance->is_virtual(1); + __PACKAGE__->result_source_instance->view_definition( + "SELECT cdid, artist, title FROM cd WHERE year ='2000'" + ); + + =head1 DESCRIPTION -A ResultSource is a component of a schema from which results can be directly -retrieved, most usually a table (see L) +A ResultSource is an object that represents a source of data for querying. -=head1 METHODS +This class is a base class for various specialised types of result +sources, for example L. Table is the +default result source type, so one is created for you when defining a +result class as described in the synopsis above. -=pod +More specifically, the L base class pulls in the +L component, which defines +the L method. +When called, C creates and stores an instance of +L. Luckily, to use tables as result +sources, you don't need to remember any of this. -=head2 new +Result sources representing select queries, or views, can also be +created, see L for full details. - $class->new(); +=head2 Finding result source objects - $class->new({attribute_name => value}); +As mentioned above, a result source instance is created and stored for +you when you define a L. -Creates a new ResultSource object. Not normally called directly by end users. +You can retrieve the result source at runtime in the following ways: + +=over + +=item From a Schema object: + + $schema->source($source_name); + +=item From a Row object: + + $row->result_source; + +=item From a ResultSet object: + + $rs->result_source; + +=back + +=head1 METHODS + +=pod =cut @@ -56,31 +111,33 @@ 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 +=head2 add_columns -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: +=over - __PACKAGE__->source_info({ - "_tablespace" => 'fast_disk_array_3', - "_engine" => 'InnoDB', - }); +=item Arguments: @columns -=head2 add_columns +=item Return value: The ResultSource object + +=back + + $source->add_columns(qw/col1 col2 col3/); - $table->add_columns(qw/col1 col2 col3/); + $source->add_columns('col1' => \%col1_info, 'col2' => \%col2_info, ...); - $table->add_columns('col1' => \%col1_info, 'col2' => \%col2_info, ...); +Adds columns to the result source. If supplied colname => hashref +pairs, uses the hashref as the L for that column. Repeated +calls of this method will add more columns, not replace them. -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 +by supplying an L in the column_info hash. The contents of the column_info are not set in stone. The following keys are currently recognised/used by DBIx::Class: @@ -89,61 +146,118 @@ keys are currently recognised/used by DBIx::Class: =item accessor -Use this to set the name of the accessor for this column. If unset, + { accessor => '_name' } + + # example use, replace standard accessor with one of your own: + sub name { + my ($self, $value) = @_; + + die "Name cannot contain digits!" if($value =~ /\d/); + $self->_name($value); + + return $self->_name(); + } + +Use this to set the name of the accessor method for this column. If unset, the name of the column will be used. =item data_type -This contains the column type. It is automatically filled by the -L producer, and the -L module. If you do not enter a -data_type, DBIx::Class will attempt to retrieve it from the -database for you, using L's column_info method. The values of this -key are typically upper-cased. + { data_type => 'integer' } + +This contains the column type. It is automatically filled if you use the +L producer, or the +L module. Currently there is no standard set of values for the data_type. Use whatever your database supports. =item size + { size => 20 } + The length of your column, if it is a column type that can have a size -restriction. This is currently only used by L. +restriction. This is currently only used to create tables from your +schema, see L. =item is_nullable -Set this to a true value for a columns that is allowed to contain -NULL values. This is currently only used by L. + { is_nullable => 1 } + +Set this to a true value for a columns that is allowed to contain NULL +values, default is false. This is currently only used to create tables +from your schema, see L. =item is_auto_increment + { is_auto_increment => 1 } + Set this to a true value for a column whose value is somehow -automatically set. This is used to determine which columns to empty -when cloning objects using C. It is also used by +automatically set, defaults to false. This is used to determine which +columns to empty when cloning objects using +L. It is also used by L. +=item is_numeric + + { is_numeric => 1 } + +Set this to a true or false value (not C) to explicitly specify +if this column contains numeric data. This controls how set_column +decides whether to consider a column dirty after an update: if +C is true a numeric comparison C<< != >> will take place +instead of the usual C + +If not specified the storage class will attempt to figure this out on +first access to the column, based on the column C. The +result will be cached in this attribute. + =item is_foreign_key + { is_foreign_key => 1 } + Set this to a true value for a column that contains a key from a -foreign table. This is currently only used by -L. +foreign table, defaults to false. This is currently only used to +create tables from your schema, see 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. + { default_value => \'now()' } + +Set this to the default value which will be inserted into a column 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 to create tables from your schema, see +L. + +See the note on L for more information about possible +issues related to db-side default values. =item sequence + { sequence => 'my_table_seq' } + Set this on a primary key column to the name of the sequence used to generate a new key value. If not specified, L will attempt to retrieve the name of the sequence from the database automatically. -=item extras +=item auto_nextval + +Set this to a true value for a column whose value is retrieved automatically +from a sequence or function (if supported by your Storage driver.) For a +sequence, if you do not use a trigger to get the nextval, you have to set the +L value as well. + +Also set this for MSSQL columns with the 'uniqueidentifier' +L whose values you want to automatically +generate using C, unless they are a primary key in which case this will +be done anyway. + +=item extra This is used by L and L -to add extra non-generic data to the column. For example: C<< extras +to add extra non-generic data to the column. For example: C<< extra => { unsigned => 1} >> is used by the MySQL producer to set an integer column to unsigned. For more details, see L. @@ -152,9 +266,18 @@ L. =head2 add_column - $table->add_column('col' => \%info?); +=over -Convenience alias to add_columns. +=item Arguments: $colname, \%columninfo? + +=item Return value: 1/0 (true/false) + +=back + + $source->add_column('col' => \%info); + +Add a single column and optional column info. Uses the same column +info keys as L. =cut @@ -175,11 +298,19 @@ sub add_columns { return $self; } -*add_column = \&add_columns; +sub add_column { shift->add_columns(@_); } # DO NOT CHANGE THIS TO GLOB =head2 has_column - if ($obj->has_column($col)) { ... } +=over + +=item Arguments: $colname + +=item Return value: 1/0 (true/false) + +=back + + if ($source->has_column($colname)) { ... } Returns true if the source has a column of this name, false otherwise. @@ -192,10 +323,19 @@ sub has_column { =head2 column_info - my $info = $obj->column_info($col); +=over + +=item Arguments: $colname + +=item Return value: Hashref of info + +=back + + my $info = $source->column_info($col); -Returns the column metadata hashref for a column. See the description -of add_column for information on the contents of the hashref. +Returns the column metadata hashref for a column, as originally passed +to L. See L above for information on the +contents of the hashref. =cut @@ -229,19 +369,19 @@ sub column_info { return $self->_columns->{$column}; } -=head2 column_info_from_storage +=head2 columns -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. +=over - __PACKAGE__->column_info_from_storage(1); +=item Arguments: None -=head2 columns +=item Return value: Ordered list of column names - my @column_names = $obj->columns; +=back + + my @column_names = $source->columns; -Returns all column names in the order they were declared to add_columns. +Returns all column names in the order they were declared to L. =cut @@ -249,44 +389,65 @@ sub columns { my $self = shift; $self->throw_exception( "columns() is a read-only accessor, did you mean add_columns()?" - ) if (@_ > 1); + ) if @_; return @{$self->{_ordered_columns}||[]}; } =head2 remove_columns - $table->remove_columns(qw/col1 col2 col3/); +=over + +=item Arguments: @colnames + +=item Return value: undefined + +=back + + $source->remove_columns(qw/col1 col2 col3/); -Removes columns from the result source. +Removes the given list of columns by name, from the result source. + +B: Removing a column that is also used in the sources primary +key, or in one of the sources unique constraints, B result in a +broken result source. =head2 remove_column - $table->remove_column('col'); +=over -Convenience alias to remove_columns. +=item Arguments: $colname -=cut +=item Return value: undefined -sub remove_columns { - my ($self, @cols) = @_; +=back - return unless $self->_ordered_columns; + $source->remove_column('col'); - my $columns = $self->_columns; - my @remaining; +Remove a single column by name from the result source, similar to +L. - foreach my $col (@{$self->_ordered_columns}) { - push @remaining, $col unless grep(/$col/, @cols); - } +B: Removing a column that is also used in the sources primary +key, or in one of the sources unique constraints, B result in a +broken result source. - foreach (@cols) { +=cut + +sub remove_columns { + my ($self, @to_remove) = @_; + + my $columns = $self->_columns + or return; + + my %to_remove; + for (@to_remove) { delete $columns->{$_}; - }; + ++$to_remove{$_}; + } - $self->_ordered_columns(\@remaining); + $self->_ordered_columns([ grep { not $to_remove{$_} } @{$self->_ordered_columns} ]); } -*remove_column = \&remove_columns; +sub remove_column { shift->remove_columns(@_); } # DO NOT CHANGE THIS TO GLOB =head2 set_primary_key @@ -294,15 +455,20 @@ sub remove_columns { =item Arguments: @cols +=item Return value: undefined + =back -Defines one or more columns as primary key for this source. Should be -called after C. +Defines one or more columns as primary key for this source. Must be +called after L. -Additionally, defines a unique constraint named C. +Additionally, defines a L +named C. The primary key columns are used by L to -retrieve automatically created values from the database. +retrieve automatically created values from the database. They are also +used as default joining columns when specifying relationships, see +L. =cut @@ -320,7 +486,16 @@ sub set_primary_key { =head2 primary_columns -Read-only accessor which returns the list of primary keys. +=over 4 + +=item Arguments: None + +=item Return value: Ordered list of primary column names + +=back + +Read-only accessor which returns the list of primary keys, supplied by +L. =cut @@ -330,6 +505,14 @@ sub primary_columns { =head2 add_unique_constraint +=over 4 + +=item Arguments: $name?, \@colnames + +=item Return value: undefined + +=back + Declare a unique constraint on this source. Call once for each unique constraint. @@ -342,11 +525,16 @@ Alternatively, you can specify only the columns: __PACKAGE__->add_unique_constraint([ qw/column1 column2/ ]); -This will result in a unique constraint named C, where -C
is replaced with the table name. +This will result in a unique constraint named +C, where C
is replaced with the table +name. + +Unique constraints are used, for example, when you pass the constraint +name as the C attribute to L. Then +only columns in the constraint are searched. -Unique constraints are used, for example, when you call -L. Only columns in the constraint are searched. +Throws an error if any of the given column names do not yet exist on +the result source. =cut @@ -369,23 +557,57 @@ sub add_unique_constraint { =head2 name_unique_constraint -Return a name for a unique constraint containing the specified columns. These -names consist of the table name and each column name, separated by underscores. +=over 4 + +=item Arguments: @colnames + +=item Return value: Constraint name + +=back + + $source->table('mytable'); + $source->name_unique_constraint('col1', 'col2'); + # returns + 'mytable_col1_col2' + +Return a name for a unique constraint containing the specified +columns. The name is created by joining the table name and each column +name, using an underscore character. For example, a constraint on a table named C containing the columns C and C would result in a constraint name of C<cd_artist_title>. +This is used by L</add_unique_constraint> if you do not specify the +optional constraint name. + =cut sub name_unique_constraint { my ($self, $cols) = @_; - return join '_', $self->name, @$cols; + my $name = $self->name; + $name = $$name if (ref $name eq 'SCALAR'); + + return join '_', $name, @$cols; } =head2 unique_constraints -Read-only accessor which returns the list of unique constraints on this source. +=over 4 + +=item Arguments: None + +=item Return value: Hash of unique constraint data + +=back + + $source->unique_constraints(); + +Read-only accessor which returns a hash of unique constraints on this +source. + +The hash is keyed by constraint name, and contains an arrayref of +column names as values. =cut @@ -395,6 +617,16 @@ sub unique_constraints { =head2 unique_constraint_names +=over 4 + +=item Arguments: None + +=item Return value: Unique constraint names + +=back + + $source->unique_constraint_names(); + Returns the list of unique constraint names defined on this source. =cut @@ -409,6 +641,16 @@ sub unique_constraint_names { =head2 unique_constraint_columns +=over 4 + +=item Arguments: $constraintname + +=item Return value: List of constraint columns + +=back + + $source->unique_constraint_columns('myconstraint'); + Returns the list of columns that make up the specified unique constraint. =cut @@ -425,19 +667,222 @@ 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<DBIx::Class::Schema/create_ddl_dir> or +L<DBIx::Class::Schema/deploy>. + +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</default_sqlt_deploy_hook> is called. + +Your callback will be passed the $source object representing the +ResultSource instance being deployed, and the +L<SQL::Translator::Schema::Table> 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<DBIx::Class::Schema/sqlt_deploy_hook>. + +See L<DBIx::Class::Manual::Cookbook/Adding Indexes And Functions To +Your SQL> 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<DBIx::Class::Schema/deploy> in your Schema class +and call L<dbh_do|DBIx::Class::Storage::DBI/dbh_do>. + +=head2 default_sqlt_deploy_hook + +=over + +=item Arguments: $source, $sqlt_table + +=item Return value: undefined + +=back + +This is the sensible default for L</sqlt_deploy_callback>. + +If a method named C<sqlt_deploy_hook> 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::Schema::ResultSet::Artist; + use base 'DBIx::Class::ResultSet'; + ... + + # In the result class + __PACKAGE__->resultset_class('My::Schema::ResultSet::Artist'); + + # Or in code + $source->resultset_class('My::Schema::ResultSet::Artist'); + +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<DBIx::Class::ResultSet>, 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 + + # In the result class + __PACKAGE__->resultset_attributes({ order_by => [ 'id' ] }); + + # Or in code + $source->resultset_attributes({ order_by => [ 'id' ] }); + +Store a collection of resultset attributes, that will be set on every +L<DBIx::Class::ResultSet> produced from this result source. For a full +list see L<DBIx::Class::ResultSet/ATTRIBUTES>. + +=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 + +=item Arguments: None + +=item Return value: FROM clause + +=back + + my $from_clause = $source->from(); + Returns an expression of the source to be supplied to storage to specify retrieval from this source. In the case of a database, the required FROM clause contents. =head2 schema +=over 4 + +=item Arguments: None + +=item Return value: A schema object + +=back + + my $schema = $source->schema(); + Returns the L<DBIx::Class::Schema> object that this result source -belongs too. +belongs to. =head2 storage +=over 4 + +=item Arguments: None + +=item Return value: A Storage object + +=back + + $source->storage->debug(1); + Returns the storage handle for the current schema. See also: L<DBIx::Class::Storage> @@ -448,8 +893,20 @@ sub storage { shift->schema->storage; } =head2 add_relationship +=over 4 + +=item Arguments: $relname, $related_source_name, \%cond, [ \%attrs ] + +=item Return value: 1/true if it succeeded + +=back + $source->add_relationship('relname', 'related_source', $cond, $attrs); +L<DBIx::Class::Relationship> describes a series of methods which +create pre-defined useful types of relationships. Look there first +before using this method directly. + The relationship name can be arbitrary, but must be unique for each relationship attached to this result source. 'related_source' should be the name with which the related result source was registered with @@ -461,7 +918,7 @@ the current schema. For example: The condition C<$cond> needs to be an L<SQL::Abstract>-style representation of the join between the tables. For example, if you're -creating a rel from Author to Book, +creating a relation from Author to Book, { 'foreign.author_id' => 'self.id' } @@ -508,6 +965,9 @@ relationship. =back +Throws an exception if the condition is improperly supplied, or cannot +be resolved. + =cut sub add_relationship { @@ -516,6 +976,14 @@ sub add_relationship { unless $cond; $attrs ||= {}; + # Check foreign and self are right in cond + if ( (ref $cond ||'') eq 'HASH') { + for (keys %$cond) { + $self->throw_exception("Keys of condition should be of form 'foreign.col', not '$_'") + if /\./ && !/^foreign\./; + } + } + my %rels = %{ $self->_relationships }; $rels{$rel} = { class => $f_source_name, source => $f_source_name, @@ -538,7 +1006,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}; # @@ -550,6 +1018,16 @@ sub add_relationship { =head2 relationships +=over 4 + +=item Arguments: None + +=item Return value: List of relationship names + +=back + + my @relnames = $source->relationships(); + Returns all relationship names for this source. =cut @@ -564,10 +1042,12 @@ sub relationships { =item Arguments: $relname +=item Return value: Hashref of relation data, + =back Returns a hash of relationship information for the specified relationship -name. +name. The keys/values are as specified for L</add_relationship>. =cut @@ -582,6 +1062,8 @@ sub relationship_info { =item Arguments: $rel +=item Return value: 1/0 (true/false) + =back Returns true if the source has a relationship of this name, false otherwise. @@ -599,10 +1081,21 @@ sub has_relationship { =item Arguments: $relname +=item Return value: Hashref of relationship data + =back -Returns an array of hash references of relationship information for -the other side of the specified relationship name. +Looks through all the relationships on the source this relationship +points to, looking for one whose condition is the reverse of the +condition on this relationship. + +A common use of this is to find the name of the C<belongs_to> relation +opposing a C<has_many> relation. For definition of these look in +L<DBIx::Class::Relationship>. + +The returned hashref is keyed by the name of the opposing +relationship, and contains its data in the same manner as +L</relationship_info>. =cut @@ -629,7 +1122,7 @@ sub reverse_relationship_info { my $otherrel_info = $othertable->relationship_info($otherrel); my $back = $othertable->related_source($otherrel); - next unless $back->name eq $self->name; + next unless $back->source_name eq $self->source_name; my @othertestconds; @@ -647,27 +1140,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 - -=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 @@ -700,62 +1188,141 @@ sub compare_relationship_keys { return $found; } -=head2 resolve_join - -=over 4 - -=item Arguments: $relation +# Returns the {from} structure used to express JOIN conditions +sub _resolve_join { + my ($self, $join, $alias, $seen, $jpath, $parent_force_left) = @_; -=back + # 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 ref $seen eq 'HASH'; -Returns the join structure required for the related result source. + $self->throw_exception ('You must supply a joinpath arrayref as the 4th argument to _resolve_join') + unless ref $jpath eq 'ARRAY'; -=cut + $jpath = [@$jpath]; # copy -sub resolve_join { - my ($self, $join, $alias, $seen) = @_; - $seen ||= {}; - if (ref $join eq 'ARRAY') { - return map { $self->resolve_join($_, $alias, $seen) } @$join; - } elsif (ref $join eq 'HASH') { + if (not defined $join) { + return (); + } + elsif (ref $join eq 'ARRAY') { return map { - my $as = ($seen->{$_} ? $_.'_'.($seen->{$_}+1) : $_); - ($self->resolve_join($_, $alias, $seen), - $self->related_source($_)->resolve_join($join->{$_}, $as, $seen)); - } keys %$join; - } elsif (ref $join) { + $self->_resolve_join($_, $alias, $seen, $jpath, $parent_force_left); + } @$join; + } + elsif (ref $join eq 'HASH') { + + my @ret; + for my $rel (keys %$join) { + + my $rel_info = $self->relationship_info($rel) + or $self->throw_exception("No such relationship ${rel}"); + + my $force_left = $parent_force_left; + $force_left ||= lc($rel_info->{attrs}{join_type}||'') eq 'left'; + + # the actual seen value will be incremented by the recursion + my $as = $self->storage->relname_to_table_alias( + $rel, ($seen->{$rel} && $seen->{$rel} + 1) + ); + + push @ret, ( + $self->_resolve_join($rel, $alias, $seen, [@$jpath], $force_left), + $self->related_source($rel)->_resolve_join( + $join->{$rel}, $as, $seen, [@$jpath, { $rel => $as }], $force_left + ) + ); + } + return @ret; + + } + elsif (ref $join) { $self->throw_exception("No idea how to resolve join reftype ".ref $join); - } else { + } + 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 = $rel_info->{attrs}{join_type} || ''; - return [ { $as => $self->related_source($join)->from, - -join_type => $type }, - $self->resolve_condition($rel_info->{cond}, $as, $alias) ]; + my $as = $self->storage->relname_to_table_alias( + $join, ($count > 1 && $count) + ); + + my $rel_info = $self->relationship_info($join) + or $self->throw_exception("No such relationship ${join}"); + + my $rel_src = $self->related_source($join); + return [ { $as => $rel_src->from, + -source_handle => $rel_src->handle, + -join_type => $parent_force_left + ? 'left' + : $rel_info->{attrs}{join_type} + , + -join_path => [@$jpath, { $join => $as } ], + -is_single => ( + $rel_info->{attrs}{accessor} + && + List::Util::first { $rel_info->{attrs}{accessor} eq $_ } (qw/single filter/) + ), + -alias => $as, + -relation_chain_depth => $seen->{-relation_chain_depth} || 0, + }, + $self->_resolve_condition($rel_info->{cond}, $as, $alias) ]; } } -=head2 resolve_condition +sub pk_depends_on { + carp 'pk_depends_on is a private method, stop calling it'; + my $self = shift; + $self->_pk_depends_on (@_); +} -=over 4 +# 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) = @_; -=item Arguments: $cond, $as, $alias|$object + my $relinfo = $self->relationship_info($relname); -=back + # don't assume things if the relationship direction is specified + return $relinfo->{attrs}{is_foreign_key_constraint} + if exists ($relinfo->{attrs}{is_foreign_key_constraint}); -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. + my $cond = $relinfo->{cond}; + return 0 unless ref($cond) eq 'HASH'; -=cut + # map { foreign.foo => 'self.bar' } to { bar => 'foo' } + my $keyhash = { map { my $x = $_; $x =~ s/.*\.//; $x; } reverse %$cond }; + + # assume anything that references our PK probably is dependent on us + # rather than vice versa, unless the far side is (a) defined or (b) + # auto-increment + my $rel_source = $self->related_source($relname); + + foreach my $p ($self->primary_columns) { + if (exists $keyhash->{$p}) { + unless (defined($rel_data->{$keyhash->{$p}}) + || $rel_source->column_info($keyhash->{$p}) + ->{is_auto_increment}) { + return 0; + } + } + } + + return 1; +} 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 { my ($self, $cond, $as, $for) = @_; - #warn %$cond; if (ref $cond eq 'HASH') { my %ret; foreach my $k (keys %{$cond}) { @@ -767,10 +1334,27 @@ sub resolve_condition { $self->throw_exception("Invalid rel cond val ${v}"); if (ref $for) { # Object #warn "$self $k $for $v"; + unless ($for->has_column_loaded($v)) { + if ($for->in_storage) { + $self->throw_exception(sprintf + "Unable to resolve relationship '%s' from object %s: column '%s' not " + . 'loaded from storage (or not passed to new() prior to insert()). You ' + . 'probably need to call ->discard_changes to get the server-side defaults ' + . 'from the database.', + $as, + $for, + $v, + ); + } + return $UNRESOLVABLE_CONDITION; + } $ret{$k} = $for->get_column($v); + #$ret{$k} = $for->get_column($v) if $for->has_column_loaded($v); #warn %ret; } elsif (!defined $for) { # undef, i.e. "no object" $ret{$k} = undef; + } elsif (ref $as eq 'HASH') { # reverse hashref + $ret{$v} = $as->{$k}; } elsif (ref $as) { # reverse object $ret{$v} = $as->get_column($k); } elsif (!defined $as) { # undef, i.e. "no reverse object" @@ -781,79 +1365,37 @@ 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 :("); + die("Can't handle condition $cond 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' - #) +# 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. -=cut +sub _resolve_prefetch { + my ($self, $pre, $alias, $alias_map, $order, $collapse, $pref_path) = @_; + $pref_path ||= []; -sub resolve_prefetch { - my ($self, $pre, $alias, $seen, $order, $collapse) = @_; - $seen ||= {}; - #$alias ||= $self->name; - #warn $alias, Dumper $pre; - if( ref $pre eq 'ARRAY' ) { + if (not defined $pre) { + return (); + } + elsif( ref $pre eq 'ARRAY' ) { return - map { $self->resolve_prefetch( $_, $alias, $seen, $order, $collapse ) } + map { $self->_resolve_prefetch( $_, $alias, $alias_map, $order, $collapse, [ @$pref_path ] ) } @$pre; } elsif( ref $pre eq 'HASH' ) { my @ret = map { - $self->resolve_prefetch($_, $alias, $seen, $order, $collapse), - $self->related_source($_)->resolve_prefetch( - $pre->{$_}, "${alias}.$_", $seen, $order, $collapse) + $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; - #die Dumper \@ret; return @ret; } elsif( ref $pre ) { @@ -861,25 +1403,51 @@ sub resolve_prefetch { "don't know how to resolve prefetch reftype ".ref($pre)); } else { - my $count = ++$seen->{$pre}; - my $as = ($count > 1 ? "${pre}_${count}" : $pre); + 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') { + if ($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 explode the number of row objects retrievable via ->next or ->all. ' + . '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}}; - $collapse->{"${as_prefix}${pre}"} = \@key; my @ord = (ref($rel_info->{attrs}{order_by}) eq 'ARRAY' ? @{$rel_info->{attrs}{order_by}} - : (defined $rel_info->{attrs}{order_by} + + : (defined $rel_info->{attrs}{order_by} ? ($rel_info->{attrs}{order_by}) : ())); push(@$order, map { "${as}.$_" } (@key, @ord)); @@ -887,8 +1455,6 @@ sub resolve_prefetch { return map { [ "${as}.$_", "${as_prefix}${pre}.$_", ] } $rel_source->columns; - #warn $alias, Dumper (\@ret); - #return @ret; } } @@ -898,6 +1464,8 @@ sub resolve_prefetch { =item Arguments: $relname +=item Return value: $source + =back Returns the result source object for the given relationship. @@ -918,6 +1486,8 @@ sub related_source { =item Arguments: $relname +=item Return value: $classname + =back Returns the class name for objects in the given relationship. @@ -932,95 +1502,72 @@ sub related_class { return $self->schema->class($self->relationship_info($rel)->{source}); } -=head2 resultset - -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 - -` package My::ResultSetClass; - use base 'DBIx::Class::ResultSet'; - ... +=head2 handle - $source->resultset_class('My::ResultSet::Class'); +Obtain a new handle to this source. Returns an instance of a +L<DBIx::Class::ResultSourceHandle>. -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<DBIx::Class::ResultSet>, and set it here. +=cut -=head2 resultset_attributes +sub handle { + return DBIx::Class::ResultSourceHandle->new({ + schema => $_[0]->schema, + source_moniker => $_[0]->source_name + }); +} - $source->resultset_attributes({ order_by => [ 'id' ] }); +=head2 throw_exception -Specify here any attributes you wish to pass to your specialised resultset. +See L<DBIx::Class::Schema/"throw_exception">. =cut -sub resultset { +sub throw_exception { 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} - ); + if (defined $self->schema) { + $self->schema->throw_exception(@_); + } + else { + DBIx::Class::Exception->throw(@_); + } } -=head2 source_name +=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_name + __PACKAGE__->source_info({ + "_tablespace" => 'fast_disk_array_3', + "_engine" => 'InnoDB', + }); -=back +=head2 new -Set the name of the result source when it is loaded into a schema. -This is usefull if you want to refer to a result source by a name other than -its class name. + $class->new(); - package ArchivedBooks; - use base qw/DBIx::Class/; - __PACKAGE__->table('books_archive'); - __PACKAGE__->source_name('Books'); + $class->new({attribute_name => value}); - # from your schema... - $schema->resultset('Books')->find(1); +Creates a new ResultSource object. Not normally called directly by end users. -=head2 handle +=head2 column_info_from_storage -Obtain a new handle to this source. Returns an instance of a -L<DBIx::Class::ResultSourceHandle>. +=over -=cut +=item Arguments: 1/0 (default: 0) -sub handle { - return new DBIx::Class::ResultSourceHandle({ - schema => $_[0]->schema, - source_moniker => $_[0]->source_name - }); -} +=item Return value: 1/0 -=head2 throw_exception +=back -See L<DBIx::Class::Schema/"throw_exception">. + __PACKAGE__->column_info_from_storage(1); -=cut +Enables the on-demand automatic loading of the above column +metadata from storage as necessary. This is *deprecated*, and +should not be used. It will be removed before 1.0. -sub throw_exception { - my $self = shift; - if (defined $self->schema) { - $self->schema->throw_exception(@_); - } else { - croak(@_); - } -} =head1 AUTHORS @@ -1032,3 +1579,4 @@ You may distribute this code under the same terms as Perl itself. =cut +1;