X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FRelationship%2FBase.pm;h=a6af9db631a94ad6eb447d2a92323f69f006da49;hb=1605376709663b035385b41828ce13ae3ed45a4d;hp=e53311768c589d18ca5c78913b4e633b42dbcf44;hpb=ed7ab0f4ce1a9118ea6285ee562ef003085a6b64;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/Relationship/Base.pm b/lib/DBIx/Class/Relationship/Base.pm index e533117..a6af9db 100644 --- a/lib/DBIx/Class/Relationship/Base.pm +++ b/lib/DBIx/Class/Relationship/Base.pm @@ -3,9 +3,11 @@ package DBIx::Class::Relationship::Base; use strict; use warnings; -use Scalar::Util (); use base qw/DBIx::Class/; + +use Scalar::Util qw/weaken blessed/; use Try::Tiny; +use namespace::clean; =head1 NAME @@ -13,6 +15,17 @@ DBIx::Class::Relationship::Base - Inter-table relationships =head1 SYNOPSIS + __PACKAGE__->add_relationship( + spiders => 'My::DB::Result::Creatures', + sub { + my $args = shift; + return { + "$args->{foreign_alias}.id" => { -ident => "$args->{self_alias}.id" }, + "$args->{foreign_alias}.type" => 'arachnid' + }; + }, + ); + =head1 DESCRIPTION This class provides methods to describe the relationships between the @@ -25,50 +38,193 @@ methods, for predefined ones, look in L. =over 4 -=item Arguments: 'relname', 'Foreign::Class', $cond, $attrs +=item Arguments: 'relname', 'Foreign::Class', $condition, $attrs =back - __PACKAGE__->add_relationship('relname', 'Foreign::Class', $cond, $attrs); + __PACKAGE__->add_relationship('relname', + 'Foreign::Class', + $condition, $attrs); + +Create a custom relationship between one result source and another +source, indicated by its class name. =head3 condition -The condition needs to be an L-style representation of the -join between the tables. When resolving the condition for use in a C, -keys using the pseudo-table C are resolved to mean "the Table on the -other side of the relationship", and values using the pseudo-table C -are resolved to mean "the Table this class is representing". Other -restrictions, such as by value, sub-select and other tables, may also be -used. Please check your database for C parameter support. +The condition argument describes the C clause of the C +expression used to connect the two sources when creating SQL queries. + +To create simple equality joins, supply a hashref containing the +remote table column name as the key(s), and the local table column +name as the value(s), for example given: + + My::Schema::Author->has_many( + books => 'My::Schema::Book', + { 'foreign.author_id' => 'self.id' } + ); + +A query like: + + $author_rs->search_related('books')->next + +will result in the following C clause: + + ... FROM author me LEFT JOIN book books ON books.author_id = me.id ... + +This describes a relationship between the C table and the +C table where the C table has a column C +containing the ID value of the C. + +C and C are pseudo aliases and must be entered +literally. They will be replaced with the actual correct table alias +when the SQL is produced. + +Similarly: + + My::Schema::Book->has_many( + editions => 'My::Schema::Edition', + { + 'foreign.publisher_id' => 'self.publisher_id', + 'foreign.type_id' => 'self.type_id', + } + ); + + ... + + $book_rs->search_related('editions')->next + +will result in the C clause: + + ... FROM book me + LEFT JOIN edition editions ON + editions.publisher_id = me.publisher_id + AND editions.type_id = me.type_id ... + +This describes the relationship from C to C, where the +C table refers to a publisher and a type (e.g. "paperback"): -For example, if you're creating a relationship from C to C, where -the C table has a column C containing the ID of the C -row: +As is the default in L, the key-value pairs will be +Ced in the result. C can be achieved with an arrayref, for +example a condition like: - { 'foreign.author_id' => 'self.id' } + My::Schema::Item->has_many( + related_item_links => My::Schema::Item::Links, + [ + { 'foreign.left_itemid' => 'self.id' }, + { 'foreign.right_itemid' => 'self.id' }, + ], + ); -will result in the C clause +will translate to the following C clause: - author me JOIN book book ON book.author_id = me.id + ... FROM item me JOIN item_relations related_item_links ON + related_item_links.left_itemid = me.id + OR related_item_links.right_itemid = me.id ... -For multi-column foreign keys, you will need to specify a C-to-C -mapping for each column in the key. For example, if you're creating a -relationship from C to C, where the C table refers to a -publisher and a type (e.g. "paperback"): +This describes the relationship from C to C, where +C is a many-to-many linking table, linking items back to +themselves in a peer fashion (without a "parent-child" designation) - { - 'foreign.publisher_id' => 'self.publisher_id', - 'foreign.type_id' => 'self.type_id', +To specify joins which describe more than a simple equality of column +values, the custom join condition coderef syntax can be used. For +example: + + My::Schema::Artist->has_many( + cds_80s => 'My::Schema::CD', + sub { + my $args = shift; + + return { + "$args->{foreign_alias}.artist" => { -ident => "$args->{self_alias}.artistid" }, + "$args->{foreign_alias}.year" => { '>', "1979", '<', "1990" }, + }; + } + ); + + ... + + $artist_rs->search_related('cds_80s')->next; + +will result in the C clause: + + ... FROM artist me LEFT JOIN cd cds_80s ON + cds_80s.artist = me.artistid + AND cds_80s.year < ? + AND cds_80s.year > ? + +with the bind values: + + '1990', '1979' + +C<< $args->{foreign_alias} >> and C<< $args->{self_alias} >> are supplied the +same values that would be otherwise substituted for C and C +in the simple hashref syntax case. + +The coderef is expected to return a valid L query-structure, just +like what one would supply as the first argument to +L. The return value will be passed directly to +L and the resulting SQL will be used verbatim as the C +clause of the C statement associated with this relationship. + +While every coderef-based condition must return a valid C clause, it may +elect to additionally return a simplified join-free condition hashref when +invoked as C<< $row_object->relationship >>, as opposed to +C<< $rs->related_resultset('relationship') >>. In this case C<$row_object> is +passed to the coderef as C<< $args->{self_rowobj} >>, so a user can do the +following: + + sub { + my $args = shift; + + return ( + { + "$args->{foreign_alias}.artist" => { -ident => "$args->{self_alias}.artistid" }, + "$args->{foreign_alias}.year" => { '>', "1979", '<', "1990" }, + }, + $args->{self_rowobj} && { + "$args->{foreign_alias}.artist" => $args->{self_rowobj}->artistid, + "$args->{foreign_alias}.year" => { '>', "1979", '<', "1990" }, + }, + ); } -This will result in the C clause: +Now this code: + + my $artist = $schema->resultset("Artist")->find({ id => 4 }); + $artist->cds_80s->all; + +Can skip a C altogether and instead produce: + + SELECT cds_80s.cdid, cds_80s.artist, cds_80s.title, cds_80s.year, cds_80s.genreid, cds_80s.single_track + FROM cd cds_80s + WHERE cds_80s.artist = ? + AND cds_80s.year < ? + AND cds_80s.year > ? - book me JOIN edition edition ON edition.publisher_id = me.publisher_id - AND edition.type_id = me.type_id +With the bind values: -Each key-value pair provided in a hashref will be used as Ced conditions. -To add an Ced condition, use an arrayref of hashrefs. See the -L documentation for more details. + '4', '1990', '1979' + +Note that in order to be able to use +L<< $row->create_related|DBIx::Class::Relationship::Base/create_related >>, +the coderef must not only return as its second such a "simple" condition +hashref which does not depend on joins being available, but the hashref must +contain only plain values/deflatable objects, such that the result can be +passed directly to L. For +instance the C constraint in the above example prevents the relationship +from being used to to create related objects (an exception will be thrown). + +In order to allow the user to go truly crazy when generating a custom C +clause, the C<$args> hashref passed to the subroutine contains some extra +metadata. Currently the supplied coderef is executed as: + + $relationship_info->{cond}->({ + self_alias => The alias of the invoking resultset ('me' in case of a row object), + foreign_alias => The alias of the to-be-joined resultset (often matches relname), + self_resultsource => The invocant's resultsource, + foreign_relname => The relationship name (does *not* always match foreign_alias), + self_rowobj => The invocant itself in case of $row_obj->relationship + }); =head3 attributes @@ -91,7 +247,11 @@ Explicitly specifies the type of join to use in the relationship. Any SQL join type is valid, e.g. C or C. It will be placed in the SQL command immediately before C. -=item proxy +=item proxy =E $column | \@columns | \%column + +=over 4 + +=item \@columns An arrayref containing a list of accessors in the foreign class to create in the main class. If, for example, you do the following: @@ -107,6 +267,25 @@ Then, assuming MyDB::Schema::LinerNotes has an accessor named notes, you can do: $cd->notes('Notes go here'); # set notes -- LinerNotes object is # created if it doesn't exist +=item \%column + +A hashref where each key is the accessor you want installed in the main class, +and its value is the name of the original in the fireign class. + + MyDB::Schema::Track->belongs_to( cd => 'DBICTest::Schema::CD', 'cd', { + proxy => { cd_title => 'title' }, + }); + +This will create an accessor named C on the C<$track> row object. + +=back + +NOTE: you can pass a nested struct too, for example: + + MyDB::Schema::Track->belongs_to( cd => 'DBICTest::Schema::CD', 'cd', { + proxy => [ 'year', { cd_title => 'title' } ], + }); + =item accessor Specifies the type of accessor that should be created for the relationship. @@ -119,7 +298,7 @@ created, which calls C for the relationship. =item is_foreign_key_constraint If you are using L to create SQL for you and you find that it -is creating constraints where it shouldn't, or not creating them where it +is creating constraints where it shouldn't, or not creating them where it should, set this attribute to a true or false value to override the detection of when to create constraints. @@ -127,8 +306,8 @@ of when to create constraints. If C is true on a C relationship for an object, then when you copy the object all the related objects will -be copied too. To turn this behaviour off, pass C<< cascade_copy => 0 >> -in the C<$attr> hashref. +be copied too. To turn this behaviour off, pass C<< cascade_copy => 0 >> +in the C<$attr> hashref. The behaviour defaults to C<< cascade_copy => 1 >> for C relationships. @@ -137,7 +316,7 @@ relationships. By default, DBIx::Class cascades deletes across C, C and C relationships. You can disable this -behaviour on a per-relationship basis by supplying +behaviour on a per-relationship basis by supplying C<< cascade_delete => 0 >> in the relationship attributes. The cascaded operations are performed after the requested delete, @@ -160,14 +339,14 @@ you must arrange to do this yourself. =item on_delete / on_update If you are using L to create SQL for you, you can use these -attributes to explicitly set the desired C or C constraint -type. If not supplied the SQLT parser will attempt to infer the constraint type by +attributes to explicitly set the desired C or C constraint +type. If not supplied the SQLT parser will attempt to infer the constraint type by interrogating the attributes of the B relationship. For any 'multi' -relationship with C<< cascade_delete => 1 >>, the corresponding belongs_to -relationship will be created with an C constraint. For any +relationship with C<< cascade_delete => 1 >>, the corresponding belongs_to +relationship will be created with an C constraint. For any relationship bearing C<< cascade_copy => 1 >> the resulting belongs_to constraint will be C. If you wish to disable this autodetection, and just -use the RDBMS' default constraint type, pass C<< on_delete => undef >> or +use the RDBMS' default constraint type, pass C<< on_delete => undef >> or C<< on_delete => '' >>, and the same for C respectively. =item is_deferrable @@ -238,15 +417,21 @@ sub related_resultset { # condition resolution may fail if an incomplete master-object prefetch # is encountered - that is ok during prefetch construction (not yet in_storage) - my $cond; - try { $cond = $source->_resolve_condition( $rel_info->{cond}, $rel, $self ) } + + # if $rel_info->{cond} is a CODE, we might need to join from the + # current resultsource instead of just querying the target + # resultsource, in that case, the condition might provide an + # additional condition in order to avoid an unecessary join if + # that is at all possible. + my ($cond, $extended_cond) = try { + $source->_resolve_condition( $rel_info->{cond}, $rel, $self, $rel ) + } catch { if ($self->in_storage) { $self->throw_exception ($_); } - else { - $cond = $DBIx::Class::ResultSource::UNRESOLVABLE_CONDITION; - } + + $DBIx::Class::ResultSource::UNRESOLVABLE_CONDITION; # RV }; if ($cond eq $DBIx::Class::ResultSource::UNRESOLVABLE_CONDITION) { @@ -254,35 +439,66 @@ sub related_resultset { foreach my $rev_rel (keys %$reverse) { if ($reverse->{$rev_rel}{attrs}{accessor} && $reverse->{$rev_rel}{attrs}{accessor} eq 'multi') { $attrs->{related_objects}{$rev_rel} = [ $self ]; - Scalar::Util::weaken($attrs->{related_object}{$rev_rel}[0]); + weaken $attrs->{related_object}{$rev_rel}[0]; } else { $attrs->{related_objects}{$rev_rel} = $self; - Scalar::Util::weaken($attrs->{related_object}{$rev_rel}); + weaken $attrs->{related_object}{$rev_rel}; } } } - if (ref $cond eq 'ARRAY') { - $cond = [ map { - if (ref $_ eq 'HASH') { - my $hash; - foreach my $key (keys %$_) { - my $newkey = $key !~ /\./ ? "me.$key" : $key; - $hash->{$newkey} = $_->{$key}; + + if (ref $rel_info->{cond} eq 'CODE' && !$extended_cond) { + # since we don't have the extended condition, we need to step + # back, get a resultset for the current row and do a + # search_related there. + + my $row_srcname = $source->source_name; + my $base_rs_class = $source->resultset_class; + my $base_rs_attr = $source->resultset_attributes; + my $base_rs = $base_rs_class->new + ($source, + { + %$base_rs_attr, + alias => $source->storage->relname_to_table_alias(lc($row_srcname).'__row',1) + }); + my $alias = $base_rs->current_source_alias; + my %identity = map { ( "${alias}.${_}" => $self->get_column($_) ) } $source->primary_columns; + my $row_rs = $base_rs->search(\%identity); + my $related = $row_rs->related_resultset($rel, { %$attrs, alias => 'me' }); + $related->search($query); + + } else { + # when we have the extended condition or we have a simple + # relationship declaration, it can optimize the JOIN away by + # simply adding the identity in WHERE. + + if (ref $rel_info->{cond} eq 'CODE' && $extended_cond) { + $cond = $extended_cond; + } + + if (ref $cond eq 'ARRAY') { + $cond = [ map { + if (ref $_ eq 'HASH') { + my $hash; + foreach my $key (keys %$_) { + my $newkey = $key !~ /\./ ? "me.$key" : $key; + $hash->{$newkey} = $_->{$key}; + } + $hash; + } else { + $_; } - $hash; - } else { - $_; + } @$cond ]; + } elsif (ref $cond eq 'HASH') { + foreach my $key (grep { ! /\./ } keys %$cond) { + $cond->{"me.$key"} = delete $cond->{$key}; } - } @$cond ]; - } elsif (ref $cond eq 'HASH') { - foreach my $key (grep { ! /\./ } keys %$cond) { - $cond->{"me.$key"} = delete $cond->{$key}; } + + $query = ($query ? { '-and' => [ $cond, $query ] } : $cond); + $self->result_source->related_source($rel)->resultset->search( + $query, $attrs); } - $query = ($query ? { '-and' => [ $cond, $query ] } : $cond); - $self->result_source->related_source($rel)->resultset->search( - $query, $attrs - ); }; } @@ -305,7 +521,7 @@ sub search_related { ( $objects_rs ) = $rs->search_related_rs('relname', $cond, $attrs); -This method works exactly the same as search_related, except that +This method works exactly the same as search_related, except that it guarantees a resultset, even in list context. =cut @@ -335,9 +551,9 @@ sub count_related { my $new_obj = $obj->new_related('relname', \%col_data); Create a new item of the related foreign class. If called on a -L object, it will magically -set any foreign key columns of the new object to the related primary -key columns of the source object for you. The newly created item will +L object, it will magically +set any foreign key columns of the new object to the related primary +key columns of the source object for you. The newly created item will not be saved into your storage until you call L on it. @@ -361,6 +577,40 @@ in L for details. sub create_related { my $self = shift; my $rel = shift; + + $self->throw_exception("Can't call *_related as class methods") + unless ref $self; + + # we need to stop and check if this is at all possible. If this is + # an extended relationship with an incomplete definition, we should + # just forbid it right now. + my $rel_info = $self->result_source->relationship_info($rel); + if (ref $rel_info->{cond} eq 'CODE') { + my ($cond, $ext) = $rel_info->{cond}->({ + self_alias => 'me', + foreign_alias => $rel, + self_rowobj => $self, + self_resultsource => $self->result_source, + foreign_relname => $rel, + }); + $self->throw_exception("unable to set_from_related - no simplified condition available for '${rel}'") + unless $ext; + + # now we need to make sure all non-identity relationship + # definitions are overriden. + my ($argref) = @_; + while ( my($col, $value) = each %$ext ) { + $col =~ s/^$rel\.//; + my $vref = ref $value; + if ($vref eq 'HASH') { + if (keys(%$value) && (keys %$value)[0] ne '=' && + !exists $argref->{$col}) { + $self->throw_exception("unable to set_from_related via complex '${rel}' condition on column(s): '${col}'") + } + } + } + } + my $obj = $self->search_related($rel)->create(@_); delete $self->{related_resultsets}->{$rel}; return $obj; @@ -458,11 +708,18 @@ sub set_from_related { if (defined $f_obj) { my $f_class = $rel_info->{class}; $self->throw_exception( "Object $f_obj isn't a ".$f_class ) - unless Scalar::Util::blessed($f_obj) and $f_obj->isa($f_class); + unless blessed $f_obj and $f_obj->isa($f_class); } - $self->set_columns( - $self->result_source->_resolve_condition( - $rel_info->{cond}, $f_obj, $rel)); + + # _resolve_condition might return two hashrefs, specially in the + # current case, since we know $f_object is an object. + my ($condref1, $condref2) = $self->result_source->_resolve_condition + ($rel_info->{cond}, $f_obj, $rel, $rel); + + # if we get two condrefs, we need to use the second, otherwise we + # use the first. + $self->set_columns($condref2 ? $condref2 : $condref1); + return 1; } @@ -532,7 +789,7 @@ B relationships.> =back my $actor = $schema->resultset('Actor')->find(1); - my @roles = $schema->resultset('Role')->search({ role => + my @roles = $schema->resultset('Role')->search({ role => { '-in' => ['Fred', 'Barney'] } } ); $actor->set_roles(\@roles);