File Coverage

blib/lib/Class/DBI/Relationship/IsA.pm
Criterion Covered Total %
statement 30 233 12.8
branch 0 62 0.0
condition 0 9 0.0
subroutine 10 35 28.5
pod 3 6 50.0
total 43 345 12.4


line stmt bran cond sub pod time code
1             package Class::DBI::Relationship::IsA;
2              
3             =head1 NAME
4              
5             Class::DBI::Relationship::IsA - A Class::DBI module for 'Is A' relationships
6              
7             =head1 DESCRIPTION
8              
9             Class::DBI::Relationship::IsA Provides an Is A relationship between Class::DBI classes/tables.
10              
11             By using this module you can emulate some features of inheritance both within your database and classes through the Class::DBI API.
12              
13             NOTE: This module is still experimental, several very nasty bugs have been found (and fixed) others may still be lurking - see CAVEATS AND BUGS below.
14              
15             Warning Will Robinson!
16              
17             =head1 SYNOPSIS
18              
19             In your database (assuming mysql):
20              
21             create table person (
22             personid int primary key auto_increment,
23             firstname varchar(32),
24             initials varchar(16),
25             surname varchar(64),
26             date_of_birth datetime
27             );
28              
29             create table artist (
30             artistid int primary key auto_increment,
31             alias varchar(128),
32             person int
33             );
34              
35              
36             In your classes:
37              
38             package Music::DBI;
39             use base 'Class::DBI';
40              
41             Music::DBI->connection('dbi:mysql:dbname', 'username', 'password');
42             __PACKAGE__->add_relationship_type(is_a => 'Class::DBI::Relationship::IsA');
43              
44             Superclass:
45              
46             package Music::Person;
47             use base 'Music::DBI';
48              
49             Music::Person->table('person');
50             Music::Person->columns(All => qw/personid firstname initials surname date_of_birth/);
51             Music::Person->columns(Primary => qw/personid/); # Good practice, less likely to break IsA
52              
53             Child class:
54              
55             package Music::Artist;
56             use base 'Music::DBI';
57             use Music::Person; # required for access to Music::Person methods
58              
59             Music::Artist->table('artist');
60             Music::Artist->columns(All => qw/artistid alias/);
61             Music::Person->columns(Primary => qw/personid/); # Good practice, less likely to break IsA
62             Music::Artist->has_many(cds => 'Music::CD');
63             Music::Artist->is_a(person => 'Person'); # Music::Artist inherits accessors from Music::Person
64              
65             ... elsewhere ...
66              
67             use Music::Artist;
68             my $artist = Music::Artist->create( {firstname=>'Sarah', surname=>'Geller', alias=>'Buffy'});
69             $artist->initials('M');
70             $artist->update();
71              
72             =cut
73              
74 1     1   942 use strict;
  1         2  
  1         53  
75             our $VERSION = '0.05';
76              
77 1     1   7 use warnings;
  1         2  
  1         36  
78 1     1   16 use base qw( Class::DBI::Relationship );
  1         2  
  1         1146  
79 1     1   7092 use Class::DBI::AbstractSearch;
  1         27322  
  1         77  
80              
81 1     1   1275 use Data::Dumper;
  1         7132  
  1         830  
82              
83             sub remap_arguments {
84 0     0 1   my $proto = shift;
85 0           my $class = shift;
86 0 0         $class->_invalid_object_method('is_a()') if ref $class;
87 0 0         my $column = $class->find_column(shift)
88             or return $class->_croak("is_a needs a valid column");
89 0 0         my $f_class = shift
90             or $class->_croak("$class $column needs an associated class");
91 0           my %meths = @_;
92 0           my @f_cols;
93 0           foreach my $f_col ($f_class->all_columns) {
94 0 0         push @f_cols, $f_col
95             unless $f_col eq $f_class->primary_column;
96             }
97 0           $class->__grouper->add_group(TEMP => map { $_->name } @f_cols);
  0            
98 0           $class->__grouper->add_group(__INHERITED => map { $_->name } @f_cols);
  0            
99 0           $class->mk_classdata('__isa_rels');
100 0           $class->__isa_rels({ });
101 0           return ($class, $column, $f_class, \%meths);
102             }
103              
104             sub triggers {
105 0     0 1   my $self = shift;
106 0           $self->class->_require_class($self->foreign_class);
107 0           my $column = $self->accessor;
108             return (
109             select => $self->_inflator,
110             before_create => $self->_creator,
111             before_update => sub {
112 0 0   0     if (my $f_obj = $_[0]->$column()) { $f_obj->update }
  0            
113             },
114              
115 0           );
116             }
117              
118             sub methods {
119 0     0 1   my $self = shift;
120 0           $self->class->_require_class($self->foreign_class);
121              
122 0           my $foreign_class = $self->foreign_class;
123 0           my $class = $self->class;
124 0           warn "foreign class : $foreign_class\n";
125              
126 0           warn "getting relationships..\n";
127              
128              
129 0           my $parent_relation_fields = $self->_inject_inherited_relationships(class=>$class, foreign=>$foreign_class);
130              
131 0           my $forbidden_fields = "(id|${class}_?u?id";
132 0 0         $forbidden_fields .= ($foreign_class->columns('Primary')) ? '|' . $foreign_class->columns('Primary') .')' : ')' ;
133 0           warn "forbidden_fields : $forbidden_fields\n";
134              
135 0           my %methods;
136 0           my $acc_name = $self->accessor->name;
137 0           foreach my $f_col ($self->foreign_class->all_columns) {
138 0           warn "f_col : $f_col, acc_name : $acc_name\n";
139 0 0 0       next if ($f_col eq $acc_name or $f_col =~ /$forbidden_fields/i or $parent_relation_fields->{$f_col});
      0        
140 0 0         if ($class->can('pure_accessor_name')) {
141             # provide seperate read/write accessor, read only accessor and write only mutator
142 0           $methods{ucfirst($class->pure_accessor_name($f_col))}
143             = $methods{$class->pure_accessor_name($f_col)} = $self->_get_methods($acc_name, $f_col,'ro');
144 0           $methods{ucfirst($class->mutator_name($f_col))}
145             = $methods{$class->mutator_name($f_col)} = $self->_get_methods($acc_name, $f_col,'wo');
146 0           $methods{ucfirst($class->accessor_name($f_col))}
147             = $methods{$class->accessor_name($f_col)} = $self->_get_methods($acc_name, $f_col,'rw');
148             } else {
149 0 0         if ( $class->mutator_name($f_col) eq $class->accessor_name($f_col) ) {
150             # provide read/write accessor
151 0           $methods{ucfirst($class->accessor_name($f_col))}
152             = $methods{$class->accessor_name($f_col)} = $self->_get_methods($acc_name, $f_col,'rw');
153             } else {
154             # provide seperate read only accessor and write only mutator
155 0           $methods{ucfirst($class->accessor_name($f_col))}
156             = $methods{$class->accessor_name($f_col)} = $self->_get_methods($acc_name, $f_col,'ro');
157 0           $methods{ucfirst($class->mutator_name($f_col))}
158             = $methods{$class->mutator_name($f_col)} = $self->_get_methods($acc_name, $f_col,'wo');
159             }
160             }
161             }
162              
163 0 0         $methods{search_where} = $self->search_where if $self->class->can('search_where');
164              
165             return(
166 0           %methods,
167             search => $self->search,
168             search_like => $self->search_like,
169             all_columns => $self->all_columns,
170             );
171             }
172              
173             sub search {
174 0     0 0   my $self = shift;
175 0           my $SUPER = $self->foreign_class;
176 0           my $col = $self->accessor;
177             {
178 1     1   10 no strict "refs";
  1         3  
  1         228  
  0            
179 0           *{$self->class."::orig_search"} = \&{"Class::DBI::search"};
  0            
  0            
180             }
181             return sub {
182 0     0     my ($self, %args) = (@_);
183 0           my (%child, %parent);
184 0           foreach my $key (keys %args) {
185 0 0         $child{$key} = $args{$key} if $self->has_real_column($key);
186 0 0         $parent{$key} = $args{$key} if $SUPER->has_real_column($key);
187             }
188 0 0         if(%parent) {
189 0           return map { $self->orig_search($col => $_->id, %child)
  0            
190             } $SUPER->search(%parent);
191             } else {
192 0           return $self->orig_search(%child);
193             }
194 0           };
195             }
196              
197             sub search_like {
198 0     0 0   my $self = shift;
199 0           my $SUPER = $self->foreign_class;
200 0           my $col = $self->accessor;
201             {
202 1     1   4 no strict "refs";
  1         3  
  1         225  
  0            
203 0           *{$self->class."::orig_search_like"} = \&{"Class::DBI::search_like"};
  0            
  0            
204             }
205             return sub {
206 0     0     my ($self, %args) = (@_);
207 0           my (%child, %parent);
208 0           foreach my $key (keys %args) {
209 0 0         $child{$key} = $args{$key} if $self->has_real_column($key);
210 0 0         $parent{$key} = $args{$key} if $SUPER->has_real_column($key);
211             }
212 0 0         if(%parent) {
213 0           return map { $self->orig_search_like($col => $_->id, %child)
  0            
214             } $SUPER->search_like(%parent);
215             } else {
216 0           return $self->orig_search_like(%child);
217             }
218 0           };
219             }
220              
221             sub search_where {
222 0     0     my $self = shift;
223 0           my $SUPER = $self->foreign_class;
224 0           my $col = $self->accessor;
225             {
226 1     1   5 no strict "refs";
  1         2  
  1         407  
  0            
227 0           *{$self->class."::orig_search_where"} = \&{"Class::DBI::AbstractSearch::search_where"};
  0            
  0            
228             }
229              
230             return sub {
231 0     0     my ($self, %args) = (@_);
232 0           my (%child, %parent);
233 0           foreach my $key (keys %args) {
234 0 0         $child{$key} = $args{$key} if $self->has_real_column($key);
235 0 0         $parent{$key} = $args{$key} if $SUPER->has_real_column($key);
236             }
237 0 0         if(%parent) {
238 0           return map { $self->orig_search_where($col->name => $_->id, %child)
  0            
239             } $SUPER->search_where(%parent);
240             } else {
241 0           return $self->orig_search_where(%child);
242             }
243 0           };
244             }
245              
246             sub all_columns {
247 0     0 0   my $self = shift;
248 0           my $SUPER = $self->foreign_class;
249 0           my $col = $self->accessor;
250             {
251 1     1   7 no strict "refs";
  1         2  
  1         466  
  0            
252 0           *{$self->class."::orig_all_columns"} = \&{"Class::DBI::all_columns"};
  0            
  0            
253             }
254             return sub {
255 0     0     my $self = shift;
256 0           return ($self->orig_all_columns, $self->columns('TEMP'));
257 0           };
258             }
259              
260              
261             ################################################################################
262              
263             sub _inject_inherited_relationships {
264 0     0     my ($self,%params) = @_;
265 0           my $class = $params{class};
266 0           my $foreign_class = $params{foreign};
267 0           my $fields = {};
268              
269 0           my %current_relationships = ();
270              
271 0 0         if ($class->can('meta_info')) {
272 0           warn "class has meta_info ";
273             # warn Dumper($class->meta_info);
274 0           my $meta_info = $class->meta_info;
275 0           foreach my $relation_type ( keys %$meta_info ) {
276 0 0         next if ($relation_type eq 'is_a');
277 0           foreach my $relname (keys %{$meta_info->{$relation_type}}) {
  0            
278 0           $current_relationships{$relname} = 1;
279             }
280             }
281             }
282              
283 0 0         if ($foreign_class->can('meta_info')) {
284 0           warn "foreign class has meta_info ";
285             # warn Dumper($class->meta_info);
286 0           my $meta_info = $foreign_class->meta_info;
287 0           foreach my $relation_type ( keys %$meta_info ) {
288 0 0         next if ($relation_type eq 'is_a');
289 0           foreach my $relname (keys %{$meta_info->{$relation_type}}) {
  0            
290 0           warn "adding new relationship : $relname \n";
291 0           $fields->{$relname} = 1;
292 0           $self->_inject_inherited_method($class, $relname);
293             }
294             }
295             }
296 0           return $fields;
297             }
298              
299             sub _inject_inherited_method {
300 0     0     my ($self,$class,$accessor_name) = @_;
301 0           my $parent_accessor = $self->accessor;
302             my $method = sub {
303 0     0     warn "injected method $accessor_name , calling $accessor_name on parent via $parent_accessor \n";
304 0           warn "..called with args ", join(', ',@_), "\n";
305 0           my ($self, @args) = @_;
306 0           $self->$parent_accessor->$accessor_name(@args);
307 0           };
308             {
309 1     1   5 no strict "refs";
  1         2  
  1         909  
  0            
310 0           *{"${class}::${accessor_name}"} = $method;
  0            
311             }
312             }
313              
314             sub _creator {
315 0     0     my $proto = shift;
316 0           my $col = $proto->accessor;
317              
318             return sub {
319 0     0     my $self = shift;
320 0           my $meta = $self->meta_info(is_a => $col);
321 0           my $f_class = $meta->foreign_class;
322              
323 0           my $hash = { };
324              
325 0           foreach ($self->__grouper->group_cols('TEMP')) {
326 0 0         next unless defined($self->_attrs($_));
327 0           $hash->{$_} = $self->_attrs($_);
328             }
329 0           my $f_pk = $f_class->primary_column;
330 0 0         if ($self->_attrs($f_pk)) {
331 0           $hash->{$f_pk} = $self->_attrs($f_pk);
332             }
333              
334 0           my $f_obj = $f_class->create($hash);
335 0           $proto->_import_column_values($self, $f_class, $f_obj);
336              
337 0           return $self->_attribute_store($col => $f_obj->id);
338 0           };
339             }
340              
341             sub _inflator {
342 0     0     my $proto = shift;
343 0           my $col = $proto->accessor;
344              
345             return sub {
346 0     0     my $self = shift;
347 0           my $value = $self->$col;
348 0           my $meta = $self->meta_info(is_a => $col);
349 0           my $f_class = $meta->foreign_class;
350              
351 0 0 0       return if ref($value) and $value->isa($f_class);
352              
353 0           $value = $f_class->_simple_bless($value);
354 0           $proto->_import_column_values($self, $f_class, $value);
355              
356 0           return $self->_attribute_store($col => $value);
357 0           };
358             }
359              
360             sub _import_column_values {
361 0     0     my ($self, $class, $f_class, $f_obj) = (@_);
362 0           foreach ($f_class->all_columns) {
363 0 0         $class->_attribute_store($_, $f_obj->$_)
364             unless $_->name eq $class->primary_column->name;
365             }
366             }
367              
368             sub _set_up_class_data {
369 0     0     my $self = shift;
370 0           $self->class->_extend_class_data(__isa_rels => $self->accessor =>
371 0           [ $self->foreign_class, %{ $self->args } ]);
372 0           $self->SUPER::_set_up_class_data;
373             }
374              
375              
376             sub _get_methods {
377 0     0     my ($self, $acc_name, $f_col, $mode) = @_;
378 0           warn "_get_methods $acc_name, $f_col, $mode \n";
379 0           warn join(', ',caller());
380 0           my $method;
381             MODE: {
382 0 0         if ($mode eq 'rw') {
  0            
383             $method = sub {
384 0     0     warn "artificial method $acc_name/$f_col called with args ", join(', ',@_), "\n";
385 0           my ($self, @args) = @_;
386 0 0         if(@args) {
387 0           $self->$acc_name->$f_col(@args);
388 0           return;
389             } else {
390 0           return $self->$acc_name->$f_col;
391             }
392 0           };
393 0           last MODE;
394             }
395 0 0         if ($mode eq 'ro') {
396             $method = sub {
397 0     0     my $self = shift;
398 0           return $self->$acc_name->$f_col;
399 0           };
400 0           last MODE;
401             }
402 0 0         if ($mode eq 'wo') {
403             $method = sub {
404 0     0     my $self = shift;
405 0           $self->$acc_name->$f_col(@_);
406 0           return;
407 0           };
408 0           last MODE;
409             }
410              
411             else {
412 0           die "can't get method for mode :$mode\n";
413             }
414             } # end of MODE
415 0           return $method;
416             }
417              
418             ################################################################################
419              
420             =head1 BUGS AND CAVEATS
421              
422             * Multiple inheritance is not supported, this is unlikely to change for the forseable future
423              
424             * is_a must be called after all other cdbi relationship methods otherwise inherited methods and
425             accessors may be over-ridden or clash unexpectedly
426              
427             * non Class::DBI attributes and methods are not inherited via this module
428              
429             * The update method is called on the inherited object when the inhertiting object has update called
430              
431             * Always specify the primary key using columns(Primary => qw/../) if you don't bad things could happen, think of the movies 'Tremors', 'Poltergeist' and 'Evil Dead' all rolled into one but without any heros.
432              
433             * Very Bad Things can and may occur when using this module even if you use good practice and are cautious -- this includes but is not limited to infinite loops, memory leaks and data corruption.
434              
435             =head1 DEPENDANCIES
436              
437             L
438              
439             =head1 SEE ALSO
440              
441             L
442              
443             L
444              
445             L
446              
447             =head1 AUTHOR
448              
449             Richard Hundt, Erichard@webtk.org.ukE
450              
451             =head1 MAINTAINER
452              
453             Aaron Trevena Eaaron.trevena@droogs.orgE
454              
455             =head1 COPYRIGHT
456              
457             Licensed for use, modification and distribution under the Artistic
458             and GNU GPL licenses.
459              
460             Copyright (C) 2004 by Richard Hundt and Aaron Trevena
461              
462             This library is free software; you can redistribute it and/or modify
463             it under the same terms as Perl itself, either Perl version 5.8.1 or,
464             at your option, any later version of Perl 5 you may have available.
465              
466             =cut
467              
468              
469             ################################################################################
470             ################################################################################
471              
472             1;
473