[Dbix-class] Unexpected behavior with possible NULL foreign key relationship

Matt S Trout dbix-class at trout.me.uk
Sun Aug 24 20:15:31 BST 2008


On Thu, Aug 07, 2008 at 02:08:54PM -0500, mike wrote:
> i'm bringing this issue back up, as i've had time to look at it again.
>  first, some context:
> 
> On Fri, Mar 21, 2008 at 03:14:40PM -0700, Damon Snyder wrote:
> > Hi Everyone,
> > I'm new to the list so bear with me if I'm missing something. I have a
> > foreign key relationship where it's possible that the belongs_to
> > relationship is NULL. See the classes below. When I create a new
> > Smsmessage object e.g. like so:
> 
> [snip]
> 
> >> >  On Wed, Apr 2, 2008 at 3:52 PM, mike <pulsation at gmail.com> wrote:
> >> however, this current problem is not mysql.  i have confirmed that it
> >> only occurs
> >> on newly created objects by adding a call to discard_changes after the creation.
> >> following is the modified patch including the test that illustrates
> >> this behavior.
> >
> > (1) your mail client mangled the patch
> > (2) after unmangling it, it makes 60core and 91debug fail.
> >
> > Can I have another patch that doesn't break anything else please? :)
> 
> attached you will find my modified patch, produced against 0.08 trunk
> (r4736).  if only by virtue of being an attachment, it should not have
> been mangled by gmail or this unholy fvwm->rdesktop->win32->firefox
> setup i use.
> 
> i've been perusing around the innards, but i'm not sure where the issue
> is.  the field is not specified to create(), so Row's constructor is not
> calling store_column for it.  if the field is specified, the accessor
> returns undef, as expected.  however, if the field is not specified, the
> accessor returns an object that describes a row from that table.  if the
> table is empty, the accessor returns undef.
> 
> if the table contains multiple rows, we see the warning:
> 
> 	DBICTest::Schema::CD::genre(): Query returned more than one row.
> 	SQL that returns multiple rows is DEPRECATED for ->find and ->single
> 
> it appears that the search_related that is called performs a simple
> SELECT * FROM table.  on lib/DBIx/Class/Relationship/Accessor.pm:21-33:

I think the correct approach is to say: "if the column isn't loaded, die().
If the object hasn't been inserted yet, return a WHERE 0=1 resultset for
the relation."

Which I've done, and seems to not be overly fucked up. It's not yet
committed though because now multi_create is completely screwed, since it
relied on the bug. I need to figure out how to get DBIC to correctly
infer whether it needs to do a $self->${foo}_related call versus
$self->related_source($rel)->resultset->$foo and then fixup the info
post-insert. It's probably not amazingly hard but the multi_create code
has always been a mess of heuristics and I'm trying not to make it worse
in the process.

Watch this space. I've attached my diff so far for anybody who's interested
in following along.

-- 
      Matt S Trout       Need help with your Catalyst or DBIx::Class project?
   Technical Director                    http://www.shadowcat.co.uk/catalyst/
 Shadowcat Systems Ltd.  Want a managed development or deployment platform?
http://chainsawblues.vox.com/            http://www.shadowcat.co.uk/servers/
-------------- next part --------------
=== lib/DBIx/Class/Relationship/Base.pm
==================================================================
--- lib/DBIx/Class/Relationship/Base.pm	(revision 23206)
+++ lib/DBIx/Class/Relationship/Base.pm	(local)
@@ -190,11 +190,18 @@
       $rel_obj->{cond}, $rel, $self
     );
     if (ref $cond eq 'ARRAY') {
-      $cond = [ map { my $hash;
-        foreach my $key (keys %$_) {
-          my $newkey = $key =~ /\./ ? "me.$key" : $key;
-          $hash->{$newkey} = $_->{$key};
-        }; $hash } @$cond ];
+      $cond = [ map {
+        if (ref $_ eq 'HASH') {
+          my $hash;
+          foreach my $key (keys %$_) {
+            my $newkey = $key =~ /\./ ? "me.$key" : $key;
+            $hash->{$newkey} = $_->{$key};
+          }
+          $hash;
+        } else {
+          $_;
+        }
+      } @$cond ];
     } else {
       foreach my $key (grep { ! /\./ } keys %$cond) {
         $cond->{"me.$key"} = delete $cond->{$key};
=== lib/DBIx/Class/ResultSource.pm
==================================================================
--- lib/DBIx/Class/ResultSource.pm	(revision 23206)
+++ lib/DBIx/Class/ResultSource.pm	(local)
@@ -796,7 +796,14 @@
         $self->throw_exception("Invalid rel cond val ${v}");
       if (ref $for) { # Object
         #warn "$self $k $for $v";
-        $ret{$k} = $for->get_column($v) if $for->has_column_loaded($v);
+        unless ($for->has_column_loaded($v)) {
+          if ($for->in_storage) {
+            $self->throw_exception("Column ${v} not loaded on ${for} trying to reolve relationship");
+          }
+          return [ \'1 = 0' ];
+        }
+        $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;
=== t/66relationship.t
==================================================================
--- t/66relationship.t	(revision 23206)
+++ t/66relationship.t	(local)
@@ -212,7 +212,7 @@
 
 my $undef_artist_cd = $schema->resultset("CD")->new_result({ 'title' => 'badgers', 'year' => 2007 });
 is($undef_artist_cd->has_column_loaded('artist'), '', 'FK not loaded');
-is($undef_artist_cd->search_related('artist')->count, 3, 'open search on undef FK');
+is($undef_artist_cd->search_related('artist')->count, 0, '0=1 search when FK does not exist and object not yet in db');
 
 my $def_artist_cd = $schema->resultset("CD")->new_result({ 'title' => 'badgers', 'year' => 2007, artist => undef });
 is($def_artist_cd->has_column_loaded('artist'), 1, 'FK loaded');


More information about the DBIx-Class mailing list