fixed new many_to_many accessors and added tests and docs
Justin Guenther [Sat, 17 Jun 2006 05:53:41 +0000 (05:53 +0000)]
lib/DBIx/Class/Relationship.pm
lib/DBIx/Class/Relationship/HasMany.pm
lib/DBIx/Class/Relationship/ManyToMany.pm
t/66relationship.t

index f9f85c2..dd36166 100644 (file)
@@ -187,6 +187,12 @@ left join.
 
 =head2 many_to_many
 
+=over 4
+
+=item Arguments: $accessor_name, $link_rel_name, $foreign_rel_name
+
+=back
+
   My::DBIC::Schema::Actor->has_many( actor_roles =>
                                      'My::DBIC::Schema::ActorRoles',
                                      'actor' );
@@ -198,17 +204,49 @@ left join.
   My::DBIC::Schema::Actor->many_to_many( roles => 'actor_roles',
                                          'role' );
 
-  ...
-
-  my @role_objs = $actor->roles;
+Creates a accessors bridging two relationships; not strictly a relationship in
+its own right, although the accessor will return a resultset or collection of
+objects just as a has_many would.
 
-Creates an accessor bridging two relationships; not strictly a relationship
-in its own right, although the accessor will return a resultset or collection
-of objects just as a has_many would.
 To use many_to_many, existing relationships from the original table to the link
 table, and from the link table to the end table must already exist, these
 relation names are then used in the many_to_many call.
 
+=head3 Created accessors
+
+=head4 $rel
+
+  my $role_rs = $actor->roles;
+
+  my $role1 = $actor->roles({ name => 'role1' })->first;
+
+Returns a resultset for the table on the far-right side of the many-to-many
+relationship. (e.g., in the above example, a CD's producers).
+
+=head4 add_to_$rel
+
+  my $role = $schema->resultset('Role')->find(1);
+  $actor->add_to_roles($role);
+      # creates a My::DBIC::Schema::ActorRoles linking table row object
+
+  $actor->add_to_roles({ name => 'role1' });
+      # creates a new My::DBIC::Schema::Role row object, as well as the
+      # linking table object
+
+Adds a linking table object for the specified object, or if a hash is given
+instead the related object is created before the linking table object is
+created.
+
+=head4 remove_from_$rel
+
+  my $role = $schema->resultset('Role')->find(1);
+  $actor->remove_from_roles($role);
+      # removes $role's My::DBIC::Schema::ActorRoles linking table row object
+
+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.
+
 =cut
 
 1;
index aa46486..eeead26 100644 (file)
@@ -11,7 +11,8 @@ sub has_many {
 
   unless (ref $cond) {
     my ($pri, $too_many) = $class->primary_columns;
-    $class->throw_exception( "has_many can only infer join for a single primary key; ${class} has more" )
+    $class->throw_exception( "has_many can only infer join for a single ".
+                            "primary key; ${class} has more" )
       if $too_many;
 
     my ($f_key,$guess);
@@ -25,18 +26,20 @@ sub has_many {
     }
 
     my $f_class_loaded = eval { $f_class->columns };
-    $class->throw_exception("No such column ${f_key} on foreign class ${f_class} ($guess)")
-      if $f_class_loaded && !$f_class->has_column($f_key);
+    $class->throw_exception(
+      "No such column ${f_key} on foreign class ${f_class} ($guess)"
+    ) if $f_class_loaded && !$f_class->has_column($f_key);
       
     $cond = { "foreign.${f_key}" => "self.${pri}" };
   }
 
-  $class->add_relationship($rel, $f_class, $cond,
-                            { accessor => 'multi',
-                              join_type => 'LEFT',
-                              cascade_delete => 1,
-                              cascade_copy => 1,
-                              %{$attrs||{}} } );
+  $class->add_relationship($rel, $f_class, $cond, {
+    accessor => 'multi',
+    join_type => 'LEFT',
+    cascade_delete => 1,
+    cascade_copy => 1,
+    %{$attrs||{}}
+  });
 }
 
 1;
index a88e1b0..8b29bf4 100644 (file)
@@ -10,6 +10,9 @@ sub many_to_many {
     no strict 'refs';
     no warnings 'redefine';
 
+    my $remove_link_meth = "remove_from_$rel";
+    my $add_link_meth = "add_to_$rel";
+
     *{"${class}::${meth}"} = sub {
       my $self = shift;
       my $attrs = @_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {};
@@ -19,24 +22,35 @@ sub many_to_many {
     };
 
     *{"${class}::add_to_${meth}"} = sub {
-      my( $self, $obj ) = @_;
-      my $vals = @_ > 2 && ref $_[$#_] eq 'HASH' ? pop(@_) : {};
-      return $self->search_related($rel)->create({
-        map { $_=>$self->get_column($_) } $self->primary_columns(),
-        map { $_=>$obj->get_column($_) } $obj->primary_columns(),
-        %$vals,
-      });
+      my $self = shift;
+      @_ > 0 or $self->throw_exception(
+        "$add_link_meth needs an object or hashref"
+      );
+      my $source = $self->result_source;
+      my $schema = $source->schema;
+      my $rel_source_name = $source->relationship_info($rel)->{source};
+      my $rel_source = $schema->resultset($rel_source_name)->result_source;
+      my $f_rel_source_name = $rel_source->relationship_info($f_rel)->{source};
+      my $f_rel_rs = $schema->resultset($f_rel_source_name);
+      my $obj = ref $_[0]
+        ? ( ref $_[0] eq 'HASH' ? $f_rel_rs->create($_[0]) : $_[0] )
+        : ( $f_rel_rs->create({@_}) );
+      my $link = $self->search_related($rel)->new_result({});
+      $link->set_from_related($f_rel, $obj);
+      $link->insert();
     };
 
     *{"${class}::remove_from_${meth}"} = sub {
-      my( $self, $obj ) = @_;
-      return $self->search_related(
-        $rel,
-        {
-            map { $_=>$self->get_column($_) } $self->primary_columns(),
-            map { $_=>$obj->get_column($_) } $obj->primary_columns(),
-        },
-      )->delete();
+      my $self = shift;
+      @_ > 0 && ref $_[0] ne 'HASH'
+        or $self->throw_exception("$remove_link_meth needs an object");
+      my $obj = shift;
+      my $rel_source = $self->search_related($rel)->result_source;
+      my $cond = $rel_source->relationship_info($f_rel)->{cond};
+      my $link_cond = $rel_source->resolve_condition(
+        $cond, $obj, $f_rel
+      );
+      $self->search_related($rel, $link_cond)->delete;
     };
 
   }
index ac4c2de..868ce15 100644 (file)
@@ -7,7 +7,7 @@ use DBICTest;
 
 my $schema = DBICTest->init_schema();
 
-plan tests => 35;
+plan tests => 40;
 
 # has_a test
 my $cd = $schema->resultset("CD")->find(4);
@@ -136,6 +136,7 @@ is( $cd->producers_sorted(producerid => 3)->next->name, 'Fred The Phenotype', 's
 
 # test new many_to_many helpers
 $cd = $schema->resultset('CD')->find(2);
+is( $cd->producers->count, 0, "CD doesn't yet have any producers" );
 my $prod = $schema->resultset('Producer')->find(1);
 $cd->add_to_producers($prod);
 my $prod_rs = $cd->producers();
@@ -143,6 +144,16 @@ is( $prod_rs->count(), 1, 'many_to_many add_to_$rel($obj) count ok' );
 is( $prod_rs->first->name, 'Matt S Trout', 'many_to_many add_to_$rel($obj) ok' );
 $cd->remove_from_producers($prod);
 is( $cd->producers->count, 0, 'many_to_many remove_from_$rel($obj) ok' );
+$cd->add_to_producers({ name => 'Testy McProducer' });
+is( $prod_rs->count(), 1, 'many_to_many add_to_$rel($hash) count ok' );
+is( $prod_rs->first->name, 'Testy McProducer', 'many_to_many add_to_$rel($hash) ok' );
+
+eval { $cd->remove_from_producers({ fake => 'hash' }); };
+like( $@, qr/needs an object/, 'remove_from_$rel($hash) dies correctly' );
+
+eval { $cd->add_to_producers(); };
+like( $@, qr/needs an object or hashref/, 'add_to_$rel(undef) dies correctly' );
+
 
 # test undirected many-to-many relationship (e.g. "related artists")
 my $undir_maps = $schema->resultset("Artist")->find(1)->artist_undirected_maps;