Factored common cdbi rel features out into Relationship:: packages
Matt S Trout [Tue, 2 Aug 2005 10:54:58 +0000 (10:54 +0000)]
lib/DBIx/Class/CDBICompat/HasA.pm
lib/DBIx/Class/CDBICompat/HasMany.pm
lib/DBIx/Class/Core.pm
lib/DBIx/Class/Cursor.pm
lib/DBIx/Class/InflateColumn.pm
lib/DBIx/Class/PK.pm
lib/DBIx/Class/Relationship.pm
lib/DBIx/Class/Relationship/Accessor.pm [new file with mode: 0644]
lib/DBIx/Class/Relationship/CascadeActions.pm [new file with mode: 0644]
lib/DBIx/Class/Table.pm
t/08inflate_has_a.t [new file with mode: 0644]

index fe85214..7f86b25 100644 (file)
@@ -4,24 +4,27 @@ use strict;
 use warnings;
 
 sub has_a {
-  my ($self, $col, $f_class) = @_;
+  my ($self, $col, $f_class, %args) = @_;
   $self->throw( "No such column ${col}" ) unless $self->_columns->{$col};
   eval "require $f_class";
+  if ($args{'inflate'} || $args{'deflate'}) {
+    if (!ref $args{'inflate'}) {
+      my $meth = $args{'inflate'};
+      $args{'inflate'} = sub { $f_class->$meth(shift); };
+    }
+    if (!ref $args{'deflate'}) {
+      my $meth = $args{'deflate'};
+      $args{'deflate'} = sub { shift->$meth; };
+    }
+    $self->inflate_column($col, \%args);
+    return 1;
+  }
   my ($pri, $too_many) = keys %{ $f_class->_primaries };
   $self->throw( "has_a only works with a single primary key; ${f_class} has more" )
     if $too_many;
   $self->add_relationship($col, $f_class,
                             { "foreign.${pri}" => "self.${col}" },
-                            { _type => 'has_a' } );
-  $self->inflate_column($col,
-    { inflate => sub { 
-        my ($val, $self) = @_;
-        return ($self->search_related($col, {}, {}))[0]
-          || $f_class->new({ $pri => $val }); },
-      deflate => sub {
-        my ($val, $self) = @_;
-        $self->throw("$val isn't a $f_class") unless $val->isa($f_class);
-        return ($val->_ident_values)[0] } } );
+                            { _type => 'has_a', accessor => 'filter' } );
   return 1;
 }
 
index 3099bf6..ece8e2b 100644 (file)
@@ -39,34 +39,14 @@ sub has_many {
     unless $f_key;
   $class->throw( "No such column ${f_key} on foreign class ${f_class}" )
     unless $f_class->_columns->{$f_key};
+  $args ||= {};
+  my $cascade = not (ref $args eq 'HAS' && delete $args->{no_cascade_delete});
   $class->add_relationship($rel, $f_class,
                             { "foreign.${f_key}" => "self.${self_key}" },
-                            { _type => 'has_many', %{$args || {}} } );
-  {
-    no strict 'refs';
-    *{"${class}::${rel}"} = sub { shift->search_related($rel, @_); };
-    *{"${class}::add_to_${rel}"} = sub { shift->create_related($rel, @_); };
-  }
+                            { accessor => 'multi',
+                              ($cascade ? ('cascade_delete' => 1) : ()),
+                              %$args } );
   return 1;
 }
 
-sub delete {
-  my ($self, @rest) = @_;
-  return $self->NEXT::ACTUAL::delete(@rest) unless ref $self;
-    # I'm just ignoring this for class deletes because hell, the db should
-    # be handling this anyway. Assuming we have joins we probably actually
-    # *could* do them, but I'd rather not.
-
-  my $ret = $self->NEXT::ACTUAL::delete(@rest);
-
-  my %rels = %{ $self->_relationships };
-  my @hm = grep { $rels{$_}{attrs}{_type}
-                   && $rels{$_}{attrs}{_type} eq 'has_many' } keys %rels;
-  foreach my $has_many (@hm) {
-    unless ($rels{$has_many}->{attrs}{no_cascade_delete}) {
-      $_->delete for $self->search_related($has_many)
-    }
-  }
-  return $ret;
-}
 1;
index 672c27f..3135fa2 100644 (file)
@@ -3,7 +3,9 @@ package DBIx::Class::Core;
 use strict;
 use warnings;
 
-use base qw/DBIx::Class::Relationship
+use base qw/DBIx::Class::Relationship::Accessor
+            DBIx::Class::Relationship::CascadeActions
+            DBIx::Class::Relationship
             DBIx::Class::InflateColumn
             DBIx::Class::SQL::OrderBy
             DBIx::Class::SQL::Abstract
index 753b6bc..b0565d1 100644 (file)
@@ -47,7 +47,10 @@ sub next {
     $self->{live_sth} = 1;
   }
   my @row = $self->{sth}->fetchrow_array;
-  return unless @row;
+  unless (@row) {
+    $self->{sth}->finish if $self->{sth}->{Active};
+    return;
+  }
   $self->{pos}++;
   return $self->{class}->_row_to_object($self->{cols}, \@row);
 }
index eea026a..a3290b0 100644 (file)
@@ -12,17 +12,19 @@ sub inflate_column {
   return 1;
 }
 
-sub _inflate_column_value {
+sub _inflated_column {
   my ($self, $col, $value) = @_;
   return $value unless defined $value; # NULL is NULL is NULL
+  return $value unless exists $self->_columns->{$col}{_inflate_info};
   return $value unless exists $self->_columns->{$col}{_inflate_info}{inflate};
   my $inflate = $self->_columns->{$col}{_inflate_info}{inflate};
   return $inflate->($value, $self);
 }
 
-sub _deflate_column_value {
+sub _deflated_column {
   my ($self, $col, $value) = @_;
   return $value unless ref $value; # If it's not an object, don't touch it
+  return $value unless exists $self->_columns->{$col}{_inflate_info};
   return $value unless exists $self->_columns->{$col}{_inflate_info}{deflate};
   my $deflate = $self->_columns->{$col}{_inflate_info}{deflate};
   return $deflate->($value, $self);
@@ -32,13 +34,11 @@ sub get_inflated_column {
   my ($self, $col) = @_;
   $self->throw("$col is not an inflated column") unless
     exists $self->_columns->{$col}{_inflate_info};
-  #warn $rel;
-  #warn join(', ', %{$self->{_column_data}});
+
   return $self->{_inflated_column}{$col}
     if exists $self->{_inflated_column}{$col};
-  #my ($pri) = (keys %{$self->_relationships->{$rel}{class}->_primaries})[0];
   return $self->{_inflated_column}{$col} =
-           $self->_inflate_column_value($col, $self->get_column($col));
+           $self->_inflated_column($col, $self->get_column($col));
 }
 
 sub set_inflated_column {
@@ -54,8 +54,10 @@ sub store_inflated_column {
     delete $self->{_inflated_column}{$col};
     return $self->store_column($col, $obj);
   }
-  my $deflated = $self->_deflate_column_value($col, $obj);
+
+  my $deflated = $self->_deflated_column($col, $obj);
            # Do this now so we don't store if it's invalid
+
   $self->{_inflated_column}{$col} = $obj;
   #warn "Storing $obj: ".($obj->_ident_values)[0];
   $self->store_column($col, $deflated);
@@ -68,7 +70,7 @@ sub new {
   my %deflated;
   foreach my $key (keys %$attrs) {
     if (exists $class->_columns->{$key}{_inflate_info}) {
-      $deflated{$key} = $class->_deflate_column_value($key,
+      $deflated{$key} = $class->_deflated_column($key,
                                                         delete $attrs->{$key});
     }
   }
@@ -78,7 +80,7 @@ sub new {
 sub _cond_value {
   my ($self, $attrs, $key, $value) = @_;
   if (exists $self->_columns->{$key}) {
-    $value = $self->_deflate_column_value($key, $value);
+    $value = $self->_deflated_column($key, $value);
   }
   return $self->NEXT::ACTUAL::_cond_value($attrs, $key, $value);
 }
index 487aa94..fef88a0 100644 (file)
@@ -77,8 +77,10 @@ sub discard_changes {
     $self->in_database(0);
     return $self;
   }
-  $self->store_column($_ => $reload->get_column($_))
-    foreach keys %{$self->_columns};
+  delete @{$self}{keys %$self};
+  @{$self}{keys %$reload} = values %$reload;
+  #$self->store_column($_ => $reload->get_column($_))
+  #  foreach keys %{$self->_columns};
   return $self;
 }
 
index def2733..976d295 100644 (file)
@@ -110,7 +110,7 @@ sub search_related {
     $attrs = { %{ pop(@_) } };
   }
   my $rel_obj = $self->_relationships->{$rel};
-  $self->throw( "No such relationship ${rel}" ) unless $rel;
+  $self->throw( "No such relationship ${rel}" ) unless $rel_obj;
   $attrs = { %{$rel_obj->{attrs} || {}}, %{$attrs || {}} };
   my $s_cond;
   if (@_) {
@@ -126,13 +126,18 @@ sub search_related {
 }
 
 sub create_related {
+  my $class = shift;
+  return $class->new_related(@_)->insert;
+}
+
+sub new_related {
   my ($self, $rel, $values, $attrs) = @_;
   $self->throw( "Can't call create_related as class method" ) 
     unless ref $self;
   $self->throw( "create_related needs a hash" ) 
     unless (ref $values eq 'HASH');
   my $rel_obj = $self->_relationships->{$rel};
-  $self->throw( "No such relationship ${rel}" ) unless $rel;
+  $self->throw( "No such relationship ${rel}" ) unless $rel_obj;
   $self->throw( "Can't abstract implicit create for ${rel}, condition not a hash" )
     unless ref $rel_obj->{cond} eq 'HASH';
   $attrs = { %{$rel_obj->{attrs}}, %{$attrs || {}}, _action => 'convert' };
@@ -141,7 +146,40 @@ sub create_related {
     $self->_cond_value($attrs, $k => $v);
     $fields{$self->_cond_key($attrs, $k)} = (@{delete $attrs->{bind}})[0];
   }
-  return $rel_obj->{class}->create(\%fields);
+  return $rel_obj->{class}->new(\%fields);
+}
+
+sub find_or_create_related {
+  my $self = shift;
+  return ($self->search_related(@_))[0] || $self->create_related(@_);
+}
+
+sub set_from_related {
+  my ($self, $rel, $f_obj) = @_;
+  my $rel_obj = $self->_relationships->{$rel};
+  $self->throw( "No such relationship ${rel}" ) unless $rel_obj;
+  my $cond = $rel_obj->{cond};
+  $self->throw( "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';
+  $self->throw( "Object $f_obj isn't a ".$rel_obj->{class} )
+    unless $f_obj->isa($rel_obj->{class});
+  foreach my $key (keys %$cond) {
+    next if ref $cond->{$key}; # Skip literals and complex conditions
+    $self->throw("set_from_related can't handle $key as key")
+      unless $key =~ m/^foreign\.([^\.]+)$/;
+    my $val = $f_obj->get_column($1);
+    $self->throw("set_from_related can't handle ".$cond->{$key}." as value")
+      unless $cond->{$key} =~ m/^self\.([^\.]+)$/;
+    $self->set_column($1 => $val);
+  }
+  return 1;
+}
+
+sub update_from_related {
+  my $self = shift;
+  $self->set_from_related(@_);
+  $self->update;
 }
 
 1;
diff --git a/lib/DBIx/Class/Relationship/Accessor.pm b/lib/DBIx/Class/Relationship/Accessor.pm
new file mode 100644 (file)
index 0000000..2350b81
--- /dev/null
@@ -0,0 +1,64 @@
+package DBIx::Class::Relationship::Accessor;
+
+use strict;
+use warnings;
+
+sub add_relationship {
+  my ($class, $rel, @rest) = @_;
+  my $ret = $class->NEXT::ACTUAL::add_relationship($rel => @rest);
+  my $rel_obj = $class->_relationships->{$rel};
+  if (my $acc_type = $rel_obj->{attrs}{accessor}) {
+    $class->_add_relationship_accessor($rel => $acc_type);
+  }
+  return $ret;
+}
+
+sub _add_relationship_accessor {
+  my ($class, $rel, $acc_type) = @_;
+  my %meth;
+  if ($acc_type eq 'single') {
+    $meth{$rel} = sub {
+      my $self = shift;
+      if (@_) {
+        $self->set_from_related($rel, @_);
+        return $self->{_relationship_data}{$rel} = $_[0];
+      } elsif (exists $self->{_relationship_data}{$rel}) {
+        return $self->{_relationship_data}{$rel};
+      } else {
+        return $self->{_relationship_data}{$rel}
+                 = $self->find_or_create_related($rel, {}, {});
+      }
+    };
+  } elsif ($acc_type eq 'filter') {
+    $class->throw("No such column $rel to filter")
+       unless exists $class->_columns->{$rel};
+    my $f_class = $class->_relationships->{$rel}{class};
+    $class->inflate_column($rel,
+      { inflate => sub {
+          my ($val, $self) = @_;
+          return $self->find_or_create_related($rel, {}, {});
+        },
+        deflate => sub {
+          my ($val, $self) = @_;
+          $self->throw("$val isn't a $f_class") unless $val->isa($f_class);
+          return ($val->_ident_values)[0];
+            # WARNING: probably breaks for multi-pri sometimes. FIXME
+        }
+      }
+    );
+  } elsif ($acc_type eq 'multi') {
+    $meth{$rel} = sub { shift->search_related($rel, @_) };
+    $meth{"add_to_${rel}"} = sub { shift->create_related($rel, @_); };
+  } else {
+    $class->throw("No such relationship accessor type $acc_type");
+  }
+  {
+    no strict 'refs';
+    no warnings 'redefine';
+    foreach my $meth (keys %meth) {
+      *{"${class}::${meth}"} = $meth{$meth};
+    }
+  }
+}
+
+1;
diff --git a/lib/DBIx/Class/Relationship/CascadeActions.pm b/lib/DBIx/Class/Relationship/CascadeActions.pm
new file mode 100644 (file)
index 0000000..53f15b0
--- /dev/null
@@ -0,0 +1,35 @@
+package DBIx::Class::Relationship::CascadeActions;
+
+sub delete {
+  my ($self, @rest) = @_;
+  return $self->NEXT::ACTUAL::delete(@rest) unless ref $self;
+    # I'm just ignoring this for class deletes because hell, the db should
+    # be handling this anyway. Assuming we have joins we probably actually
+    # *could* do them, but I'd rather not.
+
+  my $ret = $self->NEXT::ACTUAL::delete(@rest);
+
+  my %rels = %{ $self->_relationships };
+  my @cascade = grep { $rels{$_}{attrs}{cascade_delete} } keys %rels;
+  foreach my $rel (@cascade) {
+    $_->delete for $self->search_related($rel);
+  }
+  return $ret;
+}
+
+sub update {
+  my ($self, @rest) = @_;
+  return $self->NEXT::ACTUAL::update(@rest) unless ref $self;
+    # Because update cascades on a class *really* don't make sense!
+
+  my $ret = $self->NEXT::ACTUAL::update(@rest);
+
+  my %rels = %{ $self->_relationships };
+  my @cascade = grep { $rels{$_}{attrs}{cascade_update} } keys %rels;
+  foreach my $rel (@cascade) {
+    $_->update for $self->$rel;
+  }
+  return $ret;
+}
+
+1;
index e6ba457..734e1d9 100644 (file)
@@ -172,9 +172,10 @@ sub retrieve_from_sql {
 sub sth_to_objects {
   my ($class, $sth, $args, $cols, $attrs) = @_;
   my @cols = ((ref $cols eq 'ARRAY') ? @$cols : @{$sth->{NAME_lc}} );
+  my @args = map { ref $_ ? ''.$_ : $_ } @$args; # Stringify objects
   my $cursor_class = $class->_cursor_class;
   eval "use $cursor_class;";
-  my $cursor = $cursor_class->new($class, $sth, $args, \@cols, $attrs);
+  my $cursor = $cursor_class->new($class, $sth, \@args, \@cols, $attrs);
   return (wantarray ? $cursor->all : $cursor);
 }
 
diff --git a/t/08inflate_has_a.t b/t/08inflate_has_a.t
new file mode 100644 (file)
index 0000000..e6bd567
--- /dev/null
@@ -0,0 +1,32 @@
+use Test::More;
+use DateTime;
+
+plan tests => 4;
+
+use lib qw(t/lib);
+
+use_ok('DBICTest');
+
+use DBIx::Class::CDBICompat::HasA;
+
+unshift(@DBICTest::ISA, 'DBIx::Class::CDBICompat::HasA');
+
+DBICTest::CD->has_a( 'year', 'DateTime',
+      inflate => sub { DateTime->new( year => shift ) },
+      deflate => sub { shift->year }
+);
+
+# inflation test
+my $cd = DBICTest::CD->retrieve(3);
+
+is( ref($cd->year), 'DateTime', 'year is a DateTime, ok' );
+
+is( $cd->year->month, 1, 'inflated month ok' );
+
+# deflate test
+my $now = DateTime->now;
+$cd->year( $now );
+$cd->update;
+
+($cd) = DBICTest::CD->search( year => $now->year );
+is( $cd->year->year, $now->year, 'deflate ok' );