X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FRelationship%2FBase.pm;h=323c31ea8440e1a700de965a688db48ff8db7e76;hb=e50536940adf2ebaef907a0c29ae37fbd5ce95b1;hp=6dcfc6781313ae32a52891ee880d8d15da5506c8;hpb=3b4c4d727a91b9091efe2b3a34193b9abf14313f;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/Relationship/Base.pm b/lib/DBIx/Class/Relationship/Base.pm index 6dcfc67..323c31e 100644 --- a/lib/DBIx/Class/Relationship/Base.pm +++ b/lib/DBIx/Class/Relationship/Base.pm @@ -7,6 +7,7 @@ use base qw/DBIx::Class/; use Scalar::Util qw/weaken blessed/; use Try::Tiny; +use DBIx::Class::_Util qw( UNRESOLVABLE_CONDITION fail_on_internal_call ); use namespace::clean; =head1 NAME @@ -38,11 +39,11 @@ methods, for predefined ones, look in L. =over 4 -=item Arguments: 'relname', 'Foreign::Class', $condition, $attrs +=item Arguments: $rel_name, $foreign_class, $condition, $attrs =back - __PACKAGE__->add_relationship('relname', + __PACKAGE__->add_relationship('rel_name', 'Foreign::Class', $condition, $attrs); @@ -54,9 +55,16 @@ source, indicated by its class name. 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: +=head4 Simple equality + +To create simple equality joins, supply a hashref containing the remote +table column name as the key(s) prefixed by C<'foreign.'>, and the +corresponding local table column name as the value(s) prefixed by C<'self.'>. +Both 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. + +For example given: My::Schema::Author->has_many( books => 'My::Schema::Book', @@ -75,10 +83,6 @@ 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( @@ -103,9 +107,11 @@ will result in the C clause: This describes the relationship from C to C, where the C table refers to a publisher and a type (e.g. "paperback"): +=head4 Multiple groups of simple equality conditions + 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: +Ced in the resulting C clause. An C can be achieved with +an arrayref. For example a condition like: My::Schema::Item->has_many( related_item_links => My::Schema::Item::Links, @@ -125,6 +131,14 @@ 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) +=head4 Custom join conditions + + NOTE: The custom join condition specification mechanism is capable of + generating JOIN clauses of virtually unlimited complexity. This may limit + your ability to traverse some of the more involved relationship chains the + way you expect, *and* may bring your RDBMS to its knees. Exercise care + when declaring relationships as described here. + To specify joins which describe more than a simple equality of column values, the custom join condition coderef syntax can be used. For example: @@ -167,11 +181,32 @@ 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<< $result->relationship >>, as opposed to -C<< $rs->related_resultset('relationship') >>. In this case C<$result> is -passed to the coderef as C<< $args->{self_rowobj} >>, so a user can do the -following: +elect to additionally return a simplified B join-free condition +consisting of a hashref with B. This boils down to two scenarios: + +=over + +=item * + +When relationship resolution is invoked after C<< $result->$rel_name >>, as +opposed to C<< $rs->related_resultset($rel_name) >>, the C<$result> object +is passed to the coderef as C<< $args->{self_result_object} >>. + +=item * + +Alternatively when the user-space invokes resolution via +C<< $result->set_from_related( $rel_name => $foreign_values_or_object ) >>, the +corresponding data is passed to the coderef as C<< $args->{foreign_values} >>, +B in the form of a hashref. If a foreign result object is supplied +(which is valid usage of L), its values will be extracted +into hashref form by calling L. + +=back + +Note that the above scenarios are mutually exclusive, that is you will be supplied +none or only one of C and C. In other words if +you define your condition coderef as: sub { my $args = shift; @@ -181,14 +216,17 @@ following: "$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->{self_result_object} ? () : { + "$args->{foreign_alias}.artist" => $args->{self_result_object}->artistid, "$args->{foreign_alias}.year" => { '>', "1979", '<', "1990" }, }, + ! $args->{foreign_values} ? () : { + "$args->{self_alias}.artistid" => $args->{foreign_values}{artist}, + } ); } -Now this code: +Then this code: my $artist = $schema->resultset("Artist")->find({ id => 4 }); $artist->cds_80s->all; @@ -205,25 +243,46 @@ With the bind values: '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). +While this code: + + my $cd = $schema->resultset("CD")->search({ artist => 1 }, { rows => 1 })->single; + my $artist = $schema->resultset("Artist")->new({}); + $artist->set_from_related('cds_80s'); + +Will properly set the C<< $artist->artistid >> field of this new object to C<1> + +Note that in order to be able to use L (and by extension +L<< $result->create_related|DBIx::Class::Relationship::Base/create_related >>), +the returned join free condition B contain only plain values/deflatable +objects. For instance the C constraint in the above example prevents +the relationship from being used to create related objects using +C<< $artst->create_related( cds_80s => { title => 'blah' } ) >> (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 result 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 a $result_object->$relationship call + self_resultsource => The resultsource instance on which rel_name is registered + rel_name => The relationship name (does *NOT* always match foreign_alias) + + self_alias => The alias of the invoking resultset + foreign_alias => The alias of the to-be-joined resultset (does *NOT* always match rel_name) + + # only one of these (or none at all) will ever be supplied to aid in the + # construction of a join-free condition + + self_result_object => The invocant *object* itself in case of a call like + $result_object->$rel_name( ... ) + + foreign_values => A *hashref* of related data: may be passed in directly or + derived via ->get_columns() from a related object in case of + $result_object->set_from_related( $rel_name, $foreign_result_object ) + + # deprecated inconsistent names, will be forever available for legacy code + self_rowobj => Old deprecated slot for self_result_object + foreign_relname => Old deprecated slot for rel_name }); =head3 attributes @@ -275,7 +334,7 @@ Then, assuming MyApp::Schema::LinerNotes has an accessor named notes, you can do For a 'belongs_to relationship, note the 'cascade_update': - MyApp::Schema::Track->belongs_to( cd => 'DBICTest::Schema::CD', 'cd, + MyApp::Schema::Track->belongs_to( cd => 'MyApp::Schema::CD', 'cd, { proxy => ['title'], cascade_update => 1 } ); $track->title('New Title'); @@ -284,9 +343,9 @@ For a 'belongs_to relationship, note the 'cascade_update': =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. +and its value is the name of the original in the foreign class. - MyApp::Schema::Track->belongs_to( cd => 'DBICTest::Schema::CD', 'cd', { + MyApp::Schema::Track->belongs_to( cd => 'MyApp::Schema::CD', 'cd', { proxy => { cd_title => 'title' }, }); @@ -296,7 +355,7 @@ This will create an accessor named C on the C<$track> result object. NOTE: you can pass a nested struct too, for example: - MyApp::Schema::Track->belongs_to( cd => 'DBICTest::Schema::CD', 'cd', { + MyApp::Schema::Track->belongs_to( cd => 'MyApp::Schema::CD', 'cd', { proxy => [ 'year', { cd_title => 'title' } ], }); @@ -347,7 +406,7 @@ the relationship attributes. The C relationship does not update across relationships by default, so if you have a 'proxy' attribute on a belongs_to and want to -use 'update' on it, you muse set C<< cascade_update => 1 >>. +use 'update' on it, you must set C<< cascade_update => 1 >>. This is not a RDMS style cascade update - it purely means that when an object has update called on it, all the related objects also @@ -423,8 +482,8 @@ $rel_name. =back # These pairs do the same thing - $row = $cd->related_resultset('artist')->single; # has_one relationship - $row = $cd->artist; + $result = $cd->related_resultset('artist')->single; # has_one relationship + $result = $cd->artist; $rs = $cd->related_resultset('tracks'); # has_many relationship $rs = $cd->tracks; @@ -440,45 +499,49 @@ this instance (like in the case of C relationships). =cut sub related_resultset { - my $self = shift; + $_[0]->throw_exception( + '$result->related_resultset() no longer accepts extra search arguments, ' + . 'you need to switch to ...->related_resultset($relname)->search_rs(...) ' + . 'instead (it was never documented and more importantly could never work ' + . 'reliably due to the heavy caching involved)' + ) if @_ > 2; - $self->throw_exception("Can't call *_related as class methods") - unless ref $self; + $_[0]->throw_exception("Can't call *_related as class methods") + unless ref $_[0]; - my $rel = shift; + return $_[0]->{related_resultsets}{$_[1]} + if defined $_[0]->{related_resultsets}{$_[1]}; - return $self->{related_resultsets}{$rel} ||= do { + my ($self, $rel) = @_; - my $rel_info = $self->relationship_info($rel) - or $self->throw_exception( "No such relationship '$rel'" ); + return $self->{related_resultsets}{$rel} = do { - my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); - $attrs = { %{$rel_info->{attrs} || {}}, %$attrs }; + my $rsrc = $self->result_source; - $self->throw_exception( "Invalid query: @_" ) - if (@_ > 1 && (@_ % 2 == 1)); - my $query = ((@_ > 1) ? {@_} : shift); + my $rel_info = $rsrc->relationship_info($rel) + or $self->throw_exception( "No such relationship '$rel'" ); - my $rsrc = $self->result_source; + my $cond_res = $rsrc->_resolve_relationship_condition( + rel_name => $rel, + self_result_object => $self, - # condition resolution may fail if an incomplete master-object prefetch - # is encountered - that is ok during prefetch construction (not yet in_storage) - my ($cond, $is_crosstable) = try { - $rsrc->_resolve_condition( $rel_info->{cond}, $rel, $self, $rel ) - } - catch { - if ($self->in_storage) { - $self->throw_exception ($_); - } + # this may look weird, but remember that we are making a resultset + # out of an existing object, with the new source being at the head + # of the FROM chain. Having a 'me' alias is nothing but expected there + foreign_alias => 'me', - $DBIx::Class::ResultSource::UNRESOLVABLE_CONDITION; # RV - }; + self_alias => "!!!\xFF()!!!_SHOULD_NEVER_BE_SEEN_IN_USE_!!!()\xFF!!!", + + # not strictly necessary, but shouldn't hurt either + require_join_free_condition => !!(ref $rel_info->{cond} ne 'CODE'), + ); # keep in mind that the following if() block is part of a do{} - no return()s!!! - if ($is_crosstable) { - $self->throw_exception ( - "A cross-table relationship condition returned for statically declared '$rel'" - ) unless ref $rel_info->{cond} eq 'CODE'; + if ( + ! $cond_res->{join_free_condition} + and + ref $rel_info->{cond} eq 'CODE' + ) { # A WHOREIFFIC hack to reinvoke the entire condition resolution # with the correct alias. Another way of doing this involves a @@ -490,20 +553,28 @@ sub related_resultset { # root alias as 'me', instead of $rel (as opposed to invoking # $rs->search_related) - local $rsrc->{_relationships}{me} = $rsrc->{_relationships}{$rel}; # make the fake 'me' rel + # make the fake 'me' rel + local $rsrc->{_relationships}{me} = { + %{ $rsrc->{_relationships}{$rel} }, + _original_name => $rel, + }; + my $obj_table_alias = lc($rsrc->source_name) . '__row'; $obj_table_alias =~ s/\W+/_/g; $rsrc->resultset->search( $self->ident_condition($obj_table_alias), { alias => $obj_table_alias }, - )->search_related('me', $query, $attrs) + )->related_resultset('me')->search(undef, $rel_info->{attrs}) } else { + # FIXME - this conditional doesn't seem correct - got to figure out # at some point what it does. Also the entire UNRESOLVABLE_CONDITION # business seems shady - we could simply not query *at all* - if ($cond eq $DBIx::Class::ResultSource::UNRESOLVABLE_CONDITION) { + my $attrs; + if ( $cond_res->{join_free_condition} eq UNRESOLVABLE_CONDITION ) { + $attrs = { %{$rel_info->{attrs}} }; my $reverse = $rsrc->reverse_relationship_info($rel); foreach my $rev_rel (keys %$reverse) { if ($reverse->{$rev_rel}{attrs}{accessor} && $reverse->{$rev_rel}{attrs}{accessor} eq 'multi') { @@ -513,29 +584,10 @@ sub related_resultset { } } } - elsif (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 { - $_; - } - } @$cond ]; - } - elsif (ref $cond eq 'HASH') { - foreach my $key (grep { ! /\./ } keys %$cond) { - $cond->{"me.$key"} = delete $cond->{$key}; - } - } - $query = ($query ? { '-and' => [ $cond, $query ] } : $cond); $rsrc->related_source($rel)->resultset->search( - $query, $attrs + $cond_res->{join_free_condition}, + $attrs || $rel_info->{attrs}, ); } }; @@ -560,7 +612,8 @@ See L for more information. =cut sub search_related { - return shift->related_resultset(shift)->search(@_); + DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS and fail_on_internal_call; + shift->related_resultset(shift)->search(@_); } =head2 search_related_rs @@ -571,7 +624,8 @@ it guarantees a resultset, even in list context. =cut sub search_related_rs { - return shift->related_resultset(shift)->search_rs(@_); + DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS and fail_on_internal_call; + shift->related_resultset(shift)->search_rs(@_) } =head2 count_related @@ -590,7 +644,8 @@ current result or where conditions. =cut sub count_related { - shift->search_related(@_)->count; + DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS and fail_on_internal_call; + shift->related_resultset(shift)->search_rs(@_)->count; } =head2 new_related @@ -611,36 +666,15 @@ your storage until you call L on it. =cut sub new_related { - my ($self, $rel, $values) = @_; - - # FIXME - this is a bad position for this (also an identical copy in - # set_from_related), but I have no saner way to hook, and I absolutely - # want this to throw at least for coderefs, instead of the "insert a NULL - # when it gets hard" insanity --ribasushi - # - # sanity check - currently throw when a complex coderef rel is encountered - # FIXME - should THROW MOAR! - - if (ref $self) { # cdbi calls this as a class method, /me vomits - - my $rsrc = $self->result_source; - my (undef, $crosstable, $cond_targets) = $rsrc->_resolve_condition ( - $rsrc->relationship_info($rel)->{cond}, $rel, $self, $rel - ); - - $self->throw_exception("Custom relationship '$rel' does not resolve to a join-free condition fragment") - if $crosstable; - - if (my @unspecified_rel_condition_chunks = grep { ! exists $values->{$_} } @{$cond_targets||[]} ) { - $self->throw_exception(sprintf ( - "Custom relationship '%s' not definitive - returns conditions instead of values for column(s): %s", - $rel, - map { "'$_'" } @unspecified_rel_condition_chunks - )); - } - } - - return $self->search_related($rel)->new_result($values); + my ($self, $rel, $data) = @_; + + $self->related_resultset($rel)->new_result( $self->result_source->_resolve_relationship_condition ( + infer_values_based_on => $data, + rel_name => $rel, + self_result_object => $self, + foreign_alias => $rel, + self_alias => 'me', + )->{inferred_values} ); } =head2 create_related @@ -688,7 +722,8 @@ See L for details. sub find_related { #my ($self, $rel, @args) = @_; - return shift->search_related(shift)->find(@_); + DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS and fail_on_internal_call; + return shift->related_resultset(shift)->find(@_); } =head2 find_or_new_related @@ -708,8 +743,9 @@ for details. sub find_or_new_related { my $self = shift; - my $obj = $self->find_related(@_); - return defined $obj ? $obj : $self->new_related(@_); + my $rel = shift; + my $obj = $self->related_resultset($rel)->find(@_); + return defined $obj ? $obj : $self->related_resultset($rel)->new_result(@_); } =head2 find_or_create_related @@ -729,8 +765,9 @@ L for details. sub find_or_create_related { my $self = shift; - my $obj = $self->find_related(@_); - return (defined($obj) ? $obj : $self->create_related(@_)); + my $rel = shift; + my $obj = $self->related_resultset($rel)->find(@_); + return (defined($obj) ? $obj : $self->related_resultset($rel)->new_result(@_)->insert); } =head2 update_or_create_related @@ -750,6 +787,7 @@ L for details. sub update_or_create_related { #my ($self, $rel, @args) = @_; + DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS and fail_on_internal_call; shift->related_resultset(shift)->update_or_create(@_); } @@ -774,44 +812,21 @@ call set_from_related on the book. This is called internally when you pass existing objects as values to L, or pass an object to a belongs_to accessor. -The columns are only set in the local copy of the object, call L to -set them in the storage. +The columns are only set in the local copy of the object, call +L to update them in the storage. =cut sub set_from_related { my ($self, $rel, $f_obj) = @_; - my $rsrc = $self->result_source; - my $rel_info = $rsrc->relationship_info($rel) - or $self->throw_exception( "No such relationship '$rel'" ); - - if (defined $f_obj) { - my $f_class = $rel_info->{class}; - $self->throw_exception( "Object '$f_obj' isn't a ".$f_class ) - unless blessed $f_obj and $f_obj->isa($f_class); - } - - - # FIXME - this is a bad position for this (also an identical copy in - # new_related), but I have no saner way to hook, and I absolutely - # want this to throw at least for coderefs, instead of the "insert a NULL - # when it gets hard" insanity --ribasushi - # - # sanity check - currently throw when a complex coderef rel is encountered - # FIXME - should THROW MOAR! - my ($cond, $crosstable, $cond_targets) = $rsrc->_resolve_condition ( - $rel_info->{cond}, $f_obj, $rel, $rel - ); - $self->throw_exception("Custom relationship '$rel' does not resolve to a join-free condition fragment") - if $crosstable; - $self->throw_exception(sprintf ( - "Custom relationship '%s' not definitive - returns conditions instead of values for column(s): %s", - $rel, - map { "'$_'" } @$cond_targets - )) if $cond_targets; - - $self->set_columns($cond); + $self->set_columns( $self->result_source->_resolve_relationship_condition ( + infer_values_based_on => {}, + rel_name => $rel, + foreign_values => $f_obj, + foreign_alias => $rel, + self_alias => 'me', + )->{inferred_values} ); return 1; } @@ -860,8 +875,9 @@ And returns the result of that. sub delete_related { my $self = shift; - my $obj = $self->search_related(@_)->delete; - delete $self->{related_resultsets}->{$_[0]}; + my $rel = shift; + my $obj = $self->related_resultset($rel)->search_rs(@_)->delete; + delete $self->{related_resultsets}->{$rel}; return $obj; } @@ -968,13 +984,16 @@ Removes the link between the current object and the related object. Note that the related object itself won't be deleted unless you call ->delete() on it. This method just removes the link between the two objects. -=head1 AUTHOR AND CONTRIBUTORS +=head1 FURTHER QUESTIONS? -See L and L in DBIx::Class +Check the list of L. -=head1 LICENSE +=head1 COPYRIGHT AND LICENSE -You may distribute this code under the same terms as Perl itself. +This module is free software L +by the L. You can +redistribute it and/or modify it under the same terms as the +L. =cut