AUTHORS mass update; mst doesn't have to take credit for -everything- :)
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Relationship / Base.pm
index 050c056..e660d14 100644 (file)
@@ -167,7 +167,7 @@ L<SQL::Abstract> and the resulting SQL will be used verbatim as the C<ON>
 clause of the C<JOIN> statement associated with this relationship.
 
 While every coderef-based condition must return a valid C<ON> clause, it may
-elect to additionally return a simplified join-free condition hashref when 
+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
@@ -249,6 +249,12 @@ command immediately before C<JOIN>.
 
 =item proxy =E<gt> $column | \@columns | \%column
 
+The 'proxy' attribute can be used to retrieve values, and to perform
+updates if the relationship has 'cascade_update' set. The 'might_have'
+and 'has_one' relationships have this set by default; if you want a proxy
+to update across a 'belongs_to' relationship, you must set the attribute
+yourself.
+
 =over 4
 
 =item \@columns
@@ -256,23 +262,31 @@ command immediately before C<JOIN>.
 An arrayref containing a list of accessors in the foreign class to create in
 the main class. If, for example, you do the following:
 
-  MyDB::Schema::CD->might_have(liner_notes => 'MyDB::Schema::LinerNotes',
+  MyApp::Schema::CD->might_have(liner_notes => 'MyApp::Schema::LinerNotes',
     undef, {
       proxy => [ qw/notes/ ],
     });
 
-Then, assuming MyDB::Schema::LinerNotes has an accessor named notes, you can do:
+Then, assuming MyApp::Schema::LinerNotes has an accessor named notes, you can do:
 
-  my $cd = MyDB::Schema::CD->find(1);
+  my $cd = MyApp::Schema::CD->find(1);
   $cd->notes('Notes go here'); # set notes -- LinerNotes object is
                                # created if it doesn't exist
 
+For a 'belongs_to relationship, note the 'cascade_update':
+
+  MyApp::Schema::Track->belongs_to( cd => 'DBICTest::Schema::CD', 'cd,
+      { proxy => ['title'], cascade_update => 1 }
+  );
+  $track->title('New Title');
+  $track->update; # updates title in CD
+
 =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', {
+  MyApp::Schema::Track->belongs_to( cd => 'DBICTest::Schema::CD', 'cd', {
       proxy => { cd_title => 'title' },
   });
 
@@ -282,7 +296,7 @@ This will create an accessor named C<cd_title> on the C<$track> row object.
 
 NOTE: you can pass a nested struct too, for example:
 
-  MyDB::Schema::Track->belongs_to( cd => 'DBICTest::Schema::CD', 'cd', {
+  MyApp::Schema::Track->belongs_to( cd => 'DBICTest::Schema::CD', 'cd', {
     proxy => [ 'year', { cd_title => 'title' } ],
   });
 
@@ -331,6 +345,10 @@ C<might_have> relationships. You can disable this behaviour on a
 per-relationship basis by supplying C<< cascade_update => 0 >> in
 the relationship attributes.
 
+The C<belongs_to> 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 >>.
+
 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
 have update called. It will not change foreign keys automatically -
@@ -417,14 +435,8 @@ 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)
-
-    # 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 )
+    my ($cond, $is_crosstable) = try {
+      $source->_resolve_condition( $rel_info->{cond}, $rel, $self, $rel )
     }
     catch {
       if ($self->in_storage) {
@@ -434,49 +446,46 @@ sub related_resultset {
       $DBIx::Class::ResultSource::UNRESOLVABLE_CONDITION;  # RV
     };
 
-    if ($cond eq $DBIx::Class::ResultSource::UNRESOLVABLE_CONDITION) {
-      my $reverse = $source->reverse_relationship_info($rel);
-      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 ];
-          weaken $attrs->{related_object}{$rev_rel}[0];
-        } else {
-          $attrs->{related_objects}{$rev_rel} = $self;
-          weaken $attrs->{related_object}{$rev_rel};
-        }
-      }
+    # 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';
+
+      # A WHOREIFFIC hack to reinvoke the entire condition resolution
+      # with the correct alias. Another way of doing this involves a
+      # lot of state passing around, and the @_ positions are already
+      # mapped out, making this crap a less icky option.
+      #
+      # The point of this exercise is to retain the spirit of the original
+      # $obj->search_related($rel) where the resulting rset will have the
+      # root alias as 'me', instead of $rel (as opposed to invoking
+      # $rs->search_related)
+
+      local $source->{_relationships}{me} = $source->{_relationships}{$rel};  # make the fake 'me' rel
+      my $obj_table_alias = lc($source->source_name) . '__row';
+      $obj_table_alias =~ s/\W+/_/g;
+
+      $source->resultset->search(
+        $self->ident_condition($obj_table_alias),
+        { alias => $obj_table_alias },
+      )->search_related('me', $query, $attrs)
     }
-
-    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;
+    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 $reverse = $source->reverse_relationship_info($rel);
+        foreach my $rev_rel (keys %$reverse) {
+          if ($reverse->{$rev_rel}{attrs}{accessor} && $reverse->{$rev_rel}{attrs}{accessor} eq 'multi') {
+            weaken($attrs->{related_objects}{$rev_rel}[0] = $self);
+          } else {
+            weaken($attrs->{related_objects}{$rev_rel} = $self);
+          }
+        }
       }
-
-      if (ref $cond eq 'ARRAY') {
+      elsif (ref $cond eq 'ARRAY') {
         $cond = [ map {
           if (ref $_ eq 'HASH') {
             my $hash;
@@ -489,15 +498,17 @@ sub related_resultset {
             $_;
           }
         } @$cond ];
-      } elsif (ref $cond eq 'HASH') {
-        foreach my $key (grep { ! /\./ } keys %$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, $attrs
+      );
     }
   };
 }
@@ -560,8 +571,36 @@ on it.
 =cut
 
 sub new_related {
-  my ($self, $rel, $values, $attrs) = @_;
-  return $self->search_related($rel)->new($values, $attrs);
+  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, $relcols) = $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 (@{$relcols || []} and @$relcols = grep { ! exists $values->{$_} } @$relcols) {
+      $self->throw_exception(sprintf (
+        "Custom relationship '%s' not definitive - returns conditions instead of values for column(s): %s",
+        $rel,
+        map { "'$_'" } @$relcols
+      ));
+    }
+  }
+
+  return $self->search_related($rel)->new_result($values);
 }
 
 =head2 create_related
@@ -577,38 +616,7 @@ in L<DBIx::Class::ResultSet> 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->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(@_);
+  my $obj = $self->new_related($rel, @_)->insert;
   delete $self->{related_resultsets}->{$rel};
   return $obj;
 }
@@ -694,28 +702,37 @@ set them in the storage.
 
 sub set_from_related {
   my ($self, $rel, $f_obj) = @_;
-  my $rel_info = $self->relationship_info($rel);
-  $self->throw_exception( "No such relationship ${rel}" ) unless $rel_info;
-  my $cond = $rel_info->{cond};
-  $self->throw_exception(
-    "set_from_related can only handle a hash condition; the ".
-    "condition for $rel is of type ".
-    (ref $cond ? ref $cond : 'plain scalar')
-  ) unless ref $cond eq 'HASH';
+
+  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);
   }
 
-  # _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);
 
-  # if we get two condrefs, we need to use the second, otherwise we
-  # use the first.
-  $self->set_columns($condref2 ? $condref2 : $condref1);
+  # 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, $relcols) = $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 { "'$_'" } @$relcols
+  )) if @{$relcols || []};
+
+  $self->set_columns($cond);
 
   return 1;
 }
@@ -827,9 +844,9 @@ 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 AUTHORS
+=head1 AUTHOR AND CONTRIBUTORS
 
-Matt S. Trout <mst@shadowcatsystems.co.uk>
+See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
 
 =head1 LICENSE