Re: disabling cascading delete

[prev] [thread] [next] [Date index for 2004/05/12]

From: Carlos Vicente
Subject: Re: disabling cascading delete
Date: 22:10 on 12 May 2004
--=-+NbIib+olWieWBrc3ud5
Content-Type: text/plain
Content-Transfer-Encoding: 7bit

> I'm not against including it in HasMany itself, as it should be safely
> backwards-compatible - although I would like to see some tests in
> that case.

I'm attaching a HasMany.pm with the extra functionality.  Also a
modified 09-has_many.t with relevant checks added.

Replacing both files in the distribution and running 'make test' should
work.

Thanks for considering this.

Regards,

cv

--=-+NbIib+olWieWBrc3ud5
Content-Disposition: attachment; filename=HasMany.pm
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; name=HasMany.pm; charset=ISO-8859-1

package Class::DBI::Relationship::HasMany;

use strict;
use warnings;

use base 'Class::DBI::Relationship';

sub remap_arguments {
    my ($proto, $class, $accessor, $f_class, $f_key, $args) =3D @_;
   =20
    my %on_delete_beh =3D ( 'cascade'  =3D> 1,
			  'set-null' =3D> 1,
			  'restrict' =3D> 1,
			  );
   =20
    return $class->_croak("has_many needs an accessor name") unless $access=
or;
    return $class->_croak("has_many needs a foreign class")  unless $f_clas=
s;
    $class->can($accessor)
	and return $class->_carp("$accessor method already exists in $class\n");
   =20
    my @f_method =3D ();
    if (ref $f_class eq "ARRAY") {
	($f_class, @f_method) =3D @$f_class;
    }
    $class->_require_class($f_class);
   =20
    if (ref $f_key eq "HASH") {    # didn't supply f_key, this is really $a=
rgs
	$args  =3D $f_key;
	$f_key =3D "";
    }
   =20
    $f_key ||=3D do {
	my $meta =3D $f_class->meta_info('has_a');
	my ($col) =3D grep $meta->{$_}->foreign_class eq $class, keys %$meta;
	$col || $class->table_alias;
    };

    if (ref $f_key eq "ARRAY") {
	return $class->_croak("Multi-column foreign keys not supported")
	    if @$f_key > 1;
	$f_key =3D $f_key->[0];
    }
   =20
    $args ||=3D {};
    $args->{mapping}     =3D \@f_method;
    $args->{foreign_key} =3D $f_key;
    $args->{order_by} ||=3D $args->{sort};   =20
    warn "sort argumemt to has_many deprecated in favour of order_by"
	if $args->{sort};            =20

    # Make 'cascade' the default on_delete behaviour
    $args->{on_delete} ||=3D "cascade";
   =20
    unless ( exists $on_delete_beh{$args->{on_delete}} ){
	return $class->_croak("Unknown on_delete behavior: $args->{on_delete}");
    }
   =20
    return ($class, $accessor, $f_class, $args);
}

sub _set_up_class_data {
    my $self =3D shift;
    $self->class->_extend_class_data(
				     __hasa_list =3D> $self->foreign_class =3D> $self->args->{foreign_k=
ey});
    $self->SUPER::_set_up_class_data;
}

sub triggers {
    my $self =3D shift;
    if ($self->args->{on_delete} eq "restrict"){
	return (
		before_delete =3D> sub {
		    if ( scalar  $self->foreign_class->search($self->args->{foreign_key} =
=3D> shift->id) ){
			return $self->class->_croak("Deletion restricted for referential integri=
ty");
		    }
		});
=09
    }elsif ($self->args->{on_delete} eq "set-null"){
	return (
		before_delete =3D> sub {
		    foreach ( $self->foreign_class->search($self->args->{foreign_key} =3D=
> shift->id) ){
			$_->set($self->args->{foreign_key}, 0);
			$_->update;
		    }
		});
    }elsif ($self->args->{on_delete} eq "cascade"){
	return (
		before_delete =3D> sub {
		    $self->foreign_class->search($self->args->{foreign_key} =3D> shift->i=
d)
			->delete_all;
		});
    }
}

sub methods {
    my $self     =3D shift;
    my $accessor =3D $self->accessor;
    return (
	    $accessor          =3D> $self->_has_many_method,
	    "add_to_$accessor" =3D> $self->_method_add_to,
	    );
}

sub _method_add_to {
    my $self     =3D shift;
    my $accessor =3D $self->accessor;
    return sub {
	my ($self, $data) =3D @_;
	my $class =3D ref $self
	    or return $self->_croak("add_to_$accessor called as class method");
	return $self->_croak("add_to_$accessor needs data")
	    unless ref $data eq "HASH";
=09
	my $meta =3D $class->meta_info(has_many =3D> $accessor);
	my ($f_class, $f_key, $args) =3D
	    ($meta->foreign_class, $meta->args->{foreign_key}, $meta->args);
	$data->{$f_key} =3D $self->id;
	$f_class->create($data);
    };
}

sub _has_many_method {
    my $self       =3D shift;
    my $run_search =3D $self->_hm_run_search;
    my @mapping    =3D @{ $self->args->{mapping} } or return $run_search;
    return sub {
	return $run_search->(@_)->set_mapping_method(@mapping)
	    unless wantarray;
	my @ret =3D $run_search->(@_);
	foreach my $meth (@mapping) { @ret =3D map $_->$meth(), @ret }
	return @ret;
    }
}

sub _hm_run_search {
    my $self =3D shift;
    my ($class, $accessor) =3D ($self->class, $self->accessor);
    return sub {
	my ($self, @search_args) =3D @_;
	my $meta =3D $class->meta_info(has_many =3D> $accessor);
	my ($f_class, $f_key, $args) =3D
	    ($meta->foreign_class, $meta->args->{foreign_key}, $meta->args);
	if (ref $self) {    # For $artist->cds
	    unshift @search_args, ($f_key =3D> $self->id);
	    push @search_args, { order_by =3D> $args->{order_by} }
	    if defined $args->{order_by};
	    return $f_class->search(@search_args);
	} else {            # For Artist->cds
	    # Cross-table join as class method
	    # This stuff is highly experimental and will probably change beyond
	    # recognition. Use at your own risk...
	    my %kv =3D @search_args;
	    my $query =3D Class::DBI::Query->new({ owner =3D> $f_class });
	    $query->kings($class, $f_class);
	    $query->add_restriction(sprintf "%s.%s =3D %s.%s",
				    $f_class->table_alias, $f_key, $class->table_alias,
				    $class->primary_column);
	    $query->add_restriction("$_ =3D ?") for keys %kv;
	    my $sth =3D $query->run(values %kv);
	    return $f_class->sth_to_objects($sth);
	}
    };
}

1;

--=-+NbIib+olWieWBrc3ud5
Content-Disposition: attachment; filename=09-has_many.t
Content-Transfer-Encoding: quoted-printable
Content-Type: text/x-troff; name=09-has_many.t; charset=ISO-8859-1

use strict;
use Test::More;

BEGIN {
	eval "use DBD::SQLite";
	plan $@ ? (skip_all =3D> 'needs DBD::SQLite for testing') : (tests =3D> 51=
);

	use lib 't/testlib';
	use Film;
	use Actor;
	use Director;
	Director->CONSTRUCT;
	Film->CONSTRUCT;
	Actor->CONSTRUCT;
	Director->has_many(directedfilms =3D> Film =3D> 'Director', { order_by =3D=
> 'title', on_delete =3D> 'restrict' });
	Director->has_many(codirectedfilms =3D> Film =3D> 'CoDirector', { order_by=
 =3D> 'title', on_delete =3D> 'set-null' });
	Film->has_many(actors =3D> Actor =3D> 'Film', { order_by =3D> 'name', on_d=
elete =3D> 'cascade' });
	Actor->has_a(Film =3D> 'Film');
	Film->has_a(Director =3D> 'Director');
	Film->has_a(CoDirector =3D> 'Director');
	is(Actor->primary_column, 'id', "Actor primary OK");
}

ok(Actor->can('Salary'), "Actor table set-up OK");
ok(Film->can('actors'), " and have a suitable actors method in Film");
ok(Director->can('IsInsane'), "Director table set-up OK");
ok(Director->can('directedfilms'), " and have a suitable directedfilms meth=
od in Director");
ok(Director->can('codirectedfilms'), " and have a suitable codirectedfilms =
method in Director");

ok(my $btaste =3D Film->retrieve('Bad Taste'), "We have Bad Taste");

ok(
	my $pvj =3D Actor->create(
		{
			Name   =3D> 'Peter Vere-Jones',
			Film   =3D> undef,
			Salary =3D> '30_000',             # For a voice!
		}
	),
	'create Actor'
);
ok(
	my $pjd =3D Director->create(
		{
			Name   =3D> 'Peter Jackson',   # Yeah, he's also the director
			Birthday   =3D> undef,
			IsInsane =3D> '1',             # no doubt
		}
	),
	'create Director'
);
ok(
	my $mj =3D Director->create(
		{
			Name   =3D> 'Michael Jackson', # whatever
			Birthday   =3D> undef,
			IsInsane =3D> '1',             # no comments
		}
	),
	'create CoDirector'
);

is $pvj->Name, "Peter Vere-Jones", "PVJ name ok";
is $pvj->Film, undef, "No film";
ok $pvj->set_Film($btaste), "Set film";
$pvj->update;
is $pvj->Film->id, $btaste->id, "Actor has film";
is $pjd->Name, "Peter Jackson", "PJ name ok";
is $mj->Name, "Michael Jackson", "MJ name ok";
ok $btaste->set('Director', $pjd), "Set Director";
ok $btaste->set('CoDirector', $mj), "Set CoDirector";
$btaste->update;
is $btaste->Director->id, $pjd->id, "Film Has Director";
is $btaste->CoDirector->id, $mj->id, "Film Has CoDirector";

{
	my @actors =3D $btaste->actors;
	is(@actors, 1, "Bad taste has one actor");
	is($actors[0]->Name, $pvj->Name, " - the correct one");
}
{
	my @dfilms =3D $pjd->directedfilms;
	is(@dfilms, 1, "PJ has directed one film");
	is($dfilms[0]->Title, $btaste->Title, " - the correct one");
}
{
	my @cdfilms =3D $mj->codirectedfilms;
	is(@cdfilms, 1, "MJ has codirected one film");
	is($cdfilms[0]->Title, $btaste->Title, " - the correct one");
}

my %pj_data =3D (
	       Name   =3D> 'Peter Jackson',
	       Salary =3D> '0',               # it's a labour of love
);

eval { my $pj =3D Film->add_to_actors(\%pj_data) };
like $@, qr/class/, "add_to_actors must be object method";

eval { my $pj =3D $btaste->add_to_actors(%pj_data) };
like $@, qr/needs/, "add_to_actors takes hash";

ok(
	my $pj =3D $btaste->add_to_actors(
		{
			Name   =3D> 'Peter Jackson',
			Salary =3D> '0',               # it's a labour of love
		}
	),
	'add_to_actors'
);


is $pj->Name,  "Peter Jackson",    "PJ ok";
is $pvj->Name, "Peter Vere-Jones", "PVJ still ok";

{
	my @actors =3D $btaste->actors;
	is @actors, 2, " - so now we have 2";
	is $actors[0]->Name, $pj->Name,  "PJ first";
	is $actors[1]->Name, $pvj->Name, "PVJ first";
}

eval {
	my @actors =3D $btaste->actors(Name =3D> $pj->Name);
	is @actors, 1, "One actor from restricted (sorted) has_many";
	is $actors[0]->Name, $pj->Name, "It's PJ";
};
is $@, '', "No errors";

my $as =3D Actor->create(
	{
		Name   =3D> 'Arnold Schwarzenegger',
		Film   =3D> 'Terminator 2',
		Salary =3D> '15_000_000'
	}
);


eval { $pjd->delete };
like $@, qr/restricted/, "delete restriction ok";

is($pjd->Name, "Peter Jackson", "Director still there");
is(($pjd->directedfilms)[0]->Title, "Bad Taste", "And his Film still there"=
);

ok($mj->delete, "Delete Michael");
my @directors =3D Director->search(Name =3D> "Michael Jackson");
is(@directors, 0, " - Michael is gone");
is($btaste->CoDirector, 0, "CoDirector is gone");

eval { $btaste->actors($pj, $pvj, $as) };
ok $@, $@;
is($btaste->actors, 2, " - so we still only have 2 actors");

my @bta_before =3D Actor->search(Film =3D> 'Bad Taste');
is(@bta_before, 2, "We have 2 actors in bad taste");
ok($btaste->delete, "Delete bad taste");
my @bta_after =3D Actor->search(Film =3D> 'Bad Taste');
is(@bta_after, 0, " - after deleting there are no actors");

# While we're here, make sure Actors have unreadable mutators and
# unwritable accessors

eval { $as->Name("Paul Reubens") };
ok $@, $@;
eval { my $name =3D $as->set_Name };
ok $@, $@;

is($as->Name, 'Arnold Schwarzenegger', "Arnie's still Arnie");


--=-+NbIib+olWieWBrc3ud5--

Re: disabling cascading delete
Carlos Vicente 22:44 on 11 May 2004

Re: disabling cascading delete
Tony Bowden 06:23 on 12 May 2004

Re: disabling cascading delete
Carlos Vicente 22:10 on 12 May 2004

Generated at 11:34 on 01 Dec 2004 by mariachi v0.52