File Coverage

blib/lib/Class/ReluctantORM/Relationship/HasManyMany.pm
Criterion Covered Total %
statement 55 521 10.5
branch 0 112 0.0
condition 0 41 0.0
subroutine 18 81 22.2
pod 12 12 100.0
total 85 767 11.0


line stmt bran cond sub pod time code
1             package Class::ReluctantORM::Relationship::HasManyMany;
2              
3             =head1 NAME
4              
5             Class::ReluctantORM::Relationship::HasManyMany
6              
7             =head1 SYNOPSIS
8              
9             # Add many-to-many relationships to a ReluctantORM Class
10              
11             # May use has_many if you provide join_table
12             Pirate->has_many(
13             class => 'Booty'
14             join_table => 'booties2pirates',
15             );
16             Pirate->has_many_many(
17             class => 'Booty'
18             method_name => 'booties',
19             # New in 0.4: multi-column keys allowed via arrayrefs
20             local_key => 'pirate_id',
21             remote_key => 'booty_id',
22             # New in 0.4: keys can have different names in the join table
23             join_local_key => 'pirate_id',
24             join_remote_key => 'booty_id',
25             join_table => 'booties2pirates',
26             join_schema => 'caribbean',
27             );
28              
29             # Now you have:
30             $booties_collection = $pirate->booties();
31              
32             # New in 0.4: in array context, implicitly do $booties_collection->all_items
33             @loot = $pirate->booties();
34              
35             # Fetchers defined automatically
36             $pirate = Pirate->fetch_with_booties($pirate_id);
37             @bipeds = Pirate->fetch_by_leg_count_with_booties(2);
38              
39             # Get info about the relationship
40             $rel = Pirate->relationships('booties');
41             $str = $rel->type(); # 'has_many_many';
42             $str = $rel->linked_class(); # 'Booty';
43             $str = $rel->linking_class(); # 'Pirate';
44             @fields = $rel->local_key_fields(); # fields in Pirate that link to join table
45             @fields = $rel->remote_key_fields(); # fields in Booty that link to join table
46              
47             $int = $rel->join_depth(); # 2
48              
49             # Class::ReluctantORM::SQL integration
50             @sql_cols = $rel->additional_output_sql_columns();
51             @cols = $rel->local_key_sql_columns();
52             @cols = $rel->remote_key_sql_columns();
53             @empty = $rel->join_local_key_sql_columns();
54             @empty = $rel->join_remote_key_sql_columns();
55              
56              
57             =head1 DESCRIPTION
58              
59             =head1 CREATING A RELATIONSHIP
60              
61             =head2 $tb_class->has_many(class => 'OtherClass', join_table => 'join_table', ....);
62              
63             =head2 $tb_class->has_many_many(class => 'OtherClass', join_table => 'join table', ...);
64              
65             Initiates a many-to-many relationship between two classes/tables.
66             Results are handled with assistance of a simple container class,
67             Class::ReluctantORM::Collection::ManyMany (documented below in this file).
68              
69             An accessor will be created named other_classes (or method_name). Note that this
70             should be plural for readability. The accessor will return a Collection object.
71              
72             Additionally, a new constructor is created, named $class->fetch_with_METHOD.
73             This constructor has the special feature that it performs an outer join and
74             prepopulates the Collection. Thus, Pirate->fetch_with_booties(23) is only
75             one DB query.
76              
77             Finally, additional constructors named $class->fetch_by_ATTRIBUTE_with_METHOD
78             will also be available via AUTOLOAD.
79              
80             Obtaining the Collection object does NOT result in trips to the database. Operations
81             on the Collection object DO require trips to the database.
82              
83             Note that a many-to-many relationship does not imply a reciprocal has_many_many relationship going the other way.
84             It's OK to set that up manually, though.
85              
86             The first form is an alias for the second form. Some users find it more readable. That
87             alias is actually provided by the HasMany module.
88              
89             In the first form, a relationship is setup to OtherClass using defaults, described below.
90              
91             In the second form, options are made explicit:
92              
93             =over
94              
95             =item class (required)
96              
97             The linked class. This is the class on the remote end of the many-to-many.
98              
99             =item join_table (required)
100              
101             The name of the join table in the database.
102              
103             =item join_schema (optional)
104              
105             The schema of the join table if different than the local class. Default: $tb_class->schema_name().
106              
107             =item method_name (optional)
108              
109             The name of the method that will be used to access the relationship. This is also the name for the relationship, which you can pass to $tb_class->relationships. Default is the lower-cased and pluralized OtherClass. For example, if you say Pirate->has_many_many(class => 'Booty', ...), you'll get $pirate->booties(). Pluralization is performed using Lingua.
110              
111             =item local_key (optional string or arrayref)
112              
113             Name or names of columns in the local table acting as keys in the link between the local table and the join table.
114             Defaults to $tb_class->primary_key_columns().
115              
116             =item remote_key (optional string or arrayref)
117              
118             Name or names of columns in the remote table acting as keys in the link between the remote table and the join table.
119             Defaults to OtherClass->primary_key_columns().
120              
121             =item join_local_key (optional string or arrayref)
122              
123             Name or names of columns in the join table acting as keys in the link between the join table and the local table.
124             Defaults to $tb_class->primary_key_columns().
125              
126             =item join_remote_key (optional string or arrayref)
127              
128             Name or names of columns in the join table acting as keys in the link between the join table and the remote table.
129             Defaults to OtherClass->primary_key_columns().
130              
131             =item join_extra_columns (optional arrayref)
132              
133             Extra columns from the join table that will be fetched.
134              
135             =back
136              
137             =cut
138              
139              
140 1     1   6 use strict;
  1         2  
  1         31  
141 1     1   6 use warnings;
  1         2  
  1         22  
142              
143 1     1   5 use Data::Dumper;
  1         1  
  1         46  
144 1     1   5 use Scalar::Util qw(blessed);
  1         1  
  1         49  
145 1     1   5 use Class::ReluctantORM::Utilities qw(install_method conditional_load array_shallow_eq check_args);
  1         2  
  1         59  
146 1     1   6 use Class::ReluctantORM::Exception;
  1         2  
  1         25  
147 1     1   5 use Class::ReluctantORM::SQL::Aliases;
  1         2  
  1         124  
148              
149             our $DEBUG = 0;
150              
151 1     1   9 use base 'Class::ReluctantORM::Relationship';
  1         2  
  1         2377  
152              
153             sub _initialize {
154 1     1   3 my $class = shift;
155 1     0   4 install_method('Class::ReluctantORM::Relationship', 'is_has_many_many', sub { return 0; });
  0     0   0  
156 1         3 install_method('Class::ReluctantORM', 'has_many_many', \&__setup_has_many_many);
157 1         3 install_method('Class::ReluctantORM', 'is_field_has_many_many', \&is_field_has_many_many);
158             }
159              
160             =head2 $str = $rel->type();
161              
162             Returns 'has_many_many'.
163              
164             =cut
165              
166 0     0 1   sub type { return 'has_many_many'; }
167              
168             =head2 $int = $rel->join_depth();
169              
170             Returns 2.
171              
172             =cut
173              
174 0     0 1   sub join_depth { 2; }
175              
176             =head2 $str = $rel->join_type();
177              
178             Returns 'LEFT OUTER'.
179              
180             This is the type of the first of the two joins - from the base table to the join table. The next join, from the join table to the remote table, is always an INNER.
181              
182             =cut
183              
184 0     0 1   sub join_type { return 'LEFT OUTER'; }
185              
186             =head2 $bool = $rel->is_has_many_many();
187              
188             Returns true.
189              
190             =cut
191              
192 0     0 1   sub is_has_many_many { return 1; }
193              
194              
195             =head2 $int = $rel->lower_multiplicity()
196              
197             Returns 0.
198              
199             =cut
200              
201 0     0 1   sub lower_multiplicity { return 0; }
202              
203             =head2 $int = $rel->upper_multiplicity()
204              
205             Returns undef.
206              
207             =cut
208              
209 0     0 1   sub upper_multiplicity { return undef; }
210              
211              
212             =begin devdocs
213              
214             Not sure this is public.... or if that calling pattern is right.
215              
216             =head2 $bool = $cro_obj->is_field_has_many_many('field');
217              
218             Returns true if the given field is a HasMany field.
219              
220             =cut
221              
222             sub is_field_has_many_many {
223 0     0     my $inv = shift;
224 0           my $field = shift;
225 0 0         my $tb_class = ref($inv) ? ref($inv) : $inv;
226 0           my $rel = $tb_class->relationships($field);
227 0 0         return $rel ? $rel->is_has_many_many() : undef;
228             }
229              
230             =head2 $bool = $rel->is_populated_in_object($cro_obj);
231              
232             Returns true if the CRO object has had this relationship fetched.
233              
234             =cut
235              
236             sub is_populated_in_object {
237 0     0 1   my $rel = shift;
238 0           my $cro_obj = shift;
239              
240             # Obtain the underlying collection
241 0           my $collection = $cro_obj->get($rel->method_name());
242 0 0         unless ($collection) {
243 0           return 0;
244             }
245              
246 0           return $collection->is_populated();
247             }
248              
249             sub _mark_unpopulated_in_object {
250 0     0     my $rel = shift;
251 0           my $cro_obj = shift;
252              
253             # Obtain the underlying collection
254 0           my $collection = $cro_obj->get($rel->method_name());
255 0 0         unless ($collection) { return; }
  0            
256 0           $collection->depopulate();
257              
258             }
259              
260              
261             # Called from ReluctantORM::new()
262             sub _handle_implicit_new {
263 0     0     my $rel = shift;
264 0           my $linking_object = shift;
265 0           my $new_args = shift;
266              
267 0           my $relation = $rel->method_name;
268 0   0       my $children = $new_args->{$relation} || undef; # Default to unpopulated
269              
270 0           my $all_exist = 1;
271 0 0 0       for my $c (@{$children || []}) { $all_exist &&= $c->is_inserted; }
  0            
  0            
272 0 0         unless ($all_exist) {
273 0           Class::ReluctantORM::Exception::Data::UnsupportedCascade->croak('Cascading imports not supported');
274             }
275              
276 0           my $collection = Class::ReluctantORM::Collection::ManyToMany->_new(
277             relationship => $rel,
278             linking_object => $linking_object,
279             );
280             # If children were provided, that's great; unfortunately we can't
281             # save them to the join table yet because we don't have keys on the parent yet
282             # So, save them to the attach queue, and save the queue later in _handle_implicit_create
283 0 0         if ($children) {
284             # So, ahhhh... is this consdiered pouplated?
285 0           $collection->{_populated} = 1; # guess so
286              
287 0           foreach my $child (@$children) {
288 0           $collection->attach($child, 1);
289             }
290 0   0       $collection->{_count} ||= 0;
291             }
292              
293              
294 0           $linking_object->set($relation, $collection);
295 0           delete $new_args->{$relation};
296             }
297              
298             # Ick.... this verges on cascading inserts. Blech.
299             # Also, this logic might be better served to be under _notify_key_change_on_linking_object
300             # (that would catch save()s as well)
301             sub _handle_implicit_create {
302 0     0     my $rel = shift;
303 0           my $linking_object = shift;
304 0           my $create_args = shift;
305              
306 0           my $method = $rel->method_name;
307 0           my $collection = $linking_object->$method;
308              
309 0           $collection->commit_pending_attachments();
310             }
311              
312             sub _notify_key_change_on_linking_object {
313 0     0     my $rel = shift;
314 0           my $changed_linking_object = shift;
315 0 0         if ($Class::ReluctantORM::SOFT_TODO_MESSAGES) {
316 0           print STDERR __PACKAGE__ . ':' . __LINE__ . " - soft TODO - HasManyMany::_notify_key_change_on_linking_object()\n";
317             }
318             }
319              
320              
321             sub __setup_has_many_many {
322 0     0     my $cro_base_class = shift;
323 0           my $hmm_class = __PACKAGE__;
324 0           my %args = ();
325              
326 0 0         if (@_ == 1) {
327 0           %args = (class => shift());
328             } else {
329 0 0         if (@_ % 2) { Class::ReluctantORM::Exception::Param::ExpectedHash->croak(); }
  0            
330 0           %args = check_args(
331             args => \@_,
332             required => [qw(class join_table)],
333             optional => [qw(
334             method_name
335             join_schema
336             remote_key
337             local_key
338             join_local_key
339             join_remote_key
340             join_extra_columns
341             )],
342             );
343             }
344              
345             # Validate Args
346 0   0       $args{method_name} ||= Class::ReluctantORM::Utilities::pluralize(Class::ReluctantORM::Utilities::camel_case_to_underscore_case((split('::', $args{class}))[-1]));
347 0   0       $args{join_schema} ||= $cro_base_class->schema_name;
348              
349             # Coerce local and foreign keys to be arrayrefs
350 0   0       $args{remote_key} ||= $args{class}->primary_key_columns();
351 0 0         $args{remote_key} = ref($args{remote_key}) eq 'ARRAY' ? $args{remote_key} : [ $args{remote_key} ];
352              
353 0   0       $args{local_key} ||= $cro_base_class->primary_key_columns();
354 0 0         $args{local_key} = ref($args{local_key}) eq 'ARRAY' ? $args{local_key} : [ $args{local_key} ];
355              
356 0   0       $args{join_remote_key} ||= $args{class}->primary_key_columns();
357 0 0         $args{join_remote_key} = ref($args{join_remote_key}) eq 'ARRAY' ? $args{join_remote_key} : [ $args{join_remote_key} ];
358              
359 0   0       $args{join_local_key} ||= $cro_base_class->primary_key_columns();
360 0 0         $args{join_local_key} = ref($args{join_local_key}) eq 'ARRAY' ? $args{join_local_key} : [ $args{join_local_key} ];
361              
362 0   0       $args{join_extra_columns} ||= [];
363              
364 0           conditional_load($args{class});
365 0           $hmm_class->delay_until_class_is_available
366             ($args{class}, $hmm_class->__relationship_installer(%args, cro_base_class => $cro_base_class));
367 0           $hmm_class->delay_until_class_is_available
368             ($args{class}, $hmm_class->__inverse_relationship_finder(%args, cro_base_class => $cro_base_class));
369              
370             }
371              
372             sub __relationship_installer {
373 0     0     my $hmm_class = shift;
374 0           my %args = @_;
375             return sub {
376 0 0   0     if ($DEBUG > 1) {
377 0           print STDERR __PACKAGE__ . ':' . __LINE__ . " - in HasManyMany setup callback\n";
378             }
379 0           my $rel = $hmm_class->new();
380 0           $rel->method_name($args{method_name});
381 0           $rel->linked_class($args{class});
382 0           $rel->linking_class($args{cro_base_class});
383 0           $rel->local_key_fields($args{cro_base_class}->field_name(@{$args{local_key}}));
  0            
384 0           $rel->remote_key_fields($args{class}->field_name(@{$args{remote_key}}));
  0            
385              
386 0           my $jt = Table->new(
387             table => $args{join_table},
388             schema => $args{join_schema},
389 0           columns => [@{$args{join_remote_key}}, @{$args{join_local_key}}, @{$args{join_extra_columns}}],
  0            
  0            
390             );
391 0           $rel->{_join_sql_table} = $jt;
392 0           $rel->{_join_remote_sql_cols} = [ map { Column->new(table => $jt, column => $_) } @{$args{join_remote_key}} ];
  0            
  0            
393 0           $rel->{_join_local_sql_cols} = [ map { Column->new(table => $jt, column => $_) } @{$args{join_local_key}} ];
  0            
  0            
394 0           $rel->remote_key_fields($args{class}->field_name(@{$args{remote_key}}));
  0            
395              
396 0           install_method($args{cro_base_class}, $args{method_name}, $rel->__make_has_many_many_accessor());
397 0           install_method($args{cro_base_class}, 'fetch_' . $args{method_name}, $rel->__make_has_many_many_fetch_accessor());
398              
399 0           $rel->_install_search_by_with_methods();
400              
401 0           my @args_copy = map { ($_, $args{$_} ) } grep { $_ ne 'cro_base_class' } keys %args;
  0            
  0            
402 0           $rel->_original_args_arrayref(\@args_copy);
403              
404 0           $args{cro_base_class}->register_relationship($rel);
405 0           };
406             }
407              
408             sub __inverse_relationship_finder {
409 0     0     my $hmm_class = shift;
410 0           my %args = @_;
411             return sub {
412 0     0     my $cro_local_class = $args{cro_base_class};
413 0           my $cro_remote_class = $args{class};
414 0           my $local_relname = $args{method_name};
415 0           my $local_rel = $cro_local_class->relationships($local_relname);
416 0 0 0       unless ($local_rel && $local_rel->is_has_many_many) { return; }
  0            
417 0 0         if ($local_rel->inverse_relationship()) {
418             # Assume we already found it
419 0           return;
420             }
421              
422             # Unlike HO and HM, HMM is self-inverting
423             # So we look for other HMM relations
424              
425             # List the has_many_many relationships on the linked class
426             # that point to this class
427 0           my @remote_hmm_rels =
428 0           grep { $_->linked_class eq $cro_local_class }
429 0           grep { $_->is_has_many_many } $cro_remote_class->relationships();
430 0 0         unless (@remote_hmm_rels) { return; }
  0            
431              
432 0           my @matches = ();
433 0           foreach my $remote_rel (@remote_hmm_rels) {
434              
435             # These are lists of keys that should be on the local table,
436             # and should be identical
437 0           my @local_keys1 = $remote_rel->remote_key_fields();
438 0           my @local_keys2 = $local_rel->local_key_fields();
439 0 0         next unless (array_shallow_eq(\@local_keys1, \@local_keys2));
440              
441             # Keys on the local side of the join table
442 0           my @join_local_keys1 = $remote_rel->join_remote_key_columns();
443 0           my @join_local_keys2 = $local_rel->join_local_key_columns();
444 0 0         next unless (array_shallow_eq(\@join_local_keys1, \@join_local_keys2));
445              
446             # Keys on the remote side of the join table
447 0           my @join_remote_keys1 = $remote_rel->join_local_key_columns();
448 0           my @join_remote_keys2 = $local_rel->join_remote_key_columns();
449 0 0         next unless (array_shallow_eq(\@join_remote_keys1, \@join_remote_keys2));
450              
451             # These are lists of keys that should be on the remote table,
452             # and should be identical
453 0           my @remote_keys1 = $remote_rel->local_key_fields();
454 0           my @remote_keys2 = $local_rel->remote_key_fields();
455 0 0         next unless (array_shallow_eq(\@remote_keys1, \@remote_keys2));
456              
457 0           push @matches, $remote_rel;
458             }
459              
460 0 0         if (@matches == 1) {
461 0           $local_rel->inverse_relationship($matches[0]);
462 0           $matches[0]->inverse_relationship($local_rel);
463             } else {
464             # Not touching that with a 10-foot pole
465             }
466              
467 0           };
468             }
469              
470              
471             =head2 @names = $rel->join_remote_key_columns();
472              
473             Returns the names of the columns on the join table that are used in the relationship to the remote table.
474              
475             =cut
476              
477 0     0 1   sub join_remote_key_columns { return map { $_->column } shift->join_remote_key_sql_columns(); }
  0            
478              
479             =head2 @names = $rel->join_local_key_columns();
480              
481             Returns the names of the columns on the join table that are used in the relationship to the local table.
482              
483             =cut
484              
485 0     0 1   sub join_local_key_columns { return map { $_->column } shift->join_local_key_sql_columns(); }
  0            
486              
487              
488             =head2 @cols = $rel->join_remote_key_sql_columns();
489              
490             Returns the columns (as Class::ReluctantORM::SQL::Column objects) on the join table that are used in the relationship to the remote table.
491              
492             =cut
493              
494 0     0 1   sub join_remote_key_sql_columns { return @{shift->{_join_remote_sql_cols}}; }
  0            
495              
496             =head2 @cols = $rel->join_local_key_sql_columns();
497              
498             Returns the columns (as Class::ReluctantORM::SQL::Column objects) on the join table that are used in the relationship to the local table.
499              
500             =cut
501              
502 0     0 1   sub join_local_key_sql_columns { return @{shift->{_join_local_sql_cols}}; }
  0            
503              
504             =head2 $table = $rel->join_sql_table();
505              
506             Returns the linking table as a Class::ReluctantORM::SQL::Table.
507              
508             =cut
509              
510 0     0 1   sub join_sql_table { return shift->{_join_sql_table}; }
511              
512             sub __make_has_many_many_accessor {
513 0     0     my $rel = shift;
514              
515             # Setup accessor
516             my $code = sub {
517 0     0     my $tb_obj = shift;
518 0           my $collection = $tb_obj->get($rel->method_name);
519 0 0         unless (defined $collection) {
520 0           $collection = Class::ReluctantORM::Collection::ManyToMany->_new(
521             relationship => $rel,
522             linking_object => $tb_obj
523             );
524 0           $tb_obj->set($rel->method_name, $collection);
525             }
526             # New feature
527 0 0         return wantarray ? $collection->all() : $collection;
528 0           };
529 0           return $code;
530             }
531              
532              
533             sub __make_has_many_many_fetch_accessor {
534 0     0     my $rel = shift;
535             return sub {
536 0     0     my $cro_obj = shift;
537 0           my $method = $rel->method_name();
538 0           $cro_obj->$method->fetch_all();
539 0           my $coll = $cro_obj->$method();
540 0 0         return wantarray ? $coll->all() : $coll;
541 0           };
542             }
543              
544              
545             # Make SQL to insert one row
546             sub __make_insert_sql {
547 0     0     my $rel = shift;
548 0           my $sql = SQL->new('INSERT');
549 0           $sql->table($rel->join_sql_table());
550              
551 0           foreach my $keycol ($rel->__join_keys()) {
552 0           $sql->add_input($keycol, Param->new());
553             }
554              
555 0           return $sql;
556             }
557              
558             # Make SQL to delete one row
559             sub __make_delete_sql {
560 0     0     my $rel = shift;
561 0           my $sql = SQL->new('DELETE');
562              
563 0           my $join_table = $rel->join_sql_table();
564 0           $sql->table($join_table);
565              
566 0           my $root_crit;
567 0           foreach my $keycol ($rel->__join_keys()) {
568 0           my $crit = Criterion->new(
569             '=',
570             $keycol,
571             Param->new(),
572             );
573 0 0         $root_crit = $root_crit ? Criterion->new('AND', $root_crit, $crit) : $crit;
574             }
575 0           $sql->where(Where->new($root_crit));
576              
577 0           return $sql;
578             }
579              
580              
581             sub __join_keys {
582 0     0     my $rel = shift;
583 0           my $sql = SQL->new('INSERT');
584 0           $sql->table($rel->join_sql_table());
585              
586 0           my @locals =
587 0           sort { $a->column cmp $b->column }
588             $rel->join_local_key_sql_columns();
589              
590 0           my @remotes =
591 0           sort { $a->column cmp $b->column }
592             $rel->join_remote_key_sql_columns();
593              
594 0           return (@locals, @remotes);
595             }
596              
597              
598             # Return array of raw values needed to be bound to execute a single-row insert or delete
599             # should be in order needed by the SQL returned by __make_insert_sql/__make_delete_sql
600             sub __make_join_binds {
601 0     0     my ($rel, $parent, $child) = @_;
602 0           my @binds;
603 0           my $use_child = 0;
604 0           foreach my $keycol ($rel->__join_keys) {
605 0   0       $use_child ||= !$parent->field_name($keycol->column);
606 0 0         my $obj = $use_child ? $child : $parent;
607 0           push @binds, $obj->raw_field_value($obj->field_name($keycol->column()));
608             }
609 0           return @binds;
610             }
611              
612             1;
613              
614              
615             #=============================================================================#
616             #=============================================================================#
617             # Collection Subclass
618             #=============================================================================#
619             #=============================================================================#
620              
621             package Class::ReluctantORM::Collection::ManyToMany;
622 1     1   6 use strict;
  1         3  
  1         27  
623 1     1   5 use warnings;
  1         2  
  1         21  
624 1     1   5 use Class::ReluctantORM::Exception;
  1         1  
  1         20  
625 1     1   4 use Class::ReluctantORM::SQL::Aliases;
  1         185  
  1         100  
626 1     1   4 use Class::ReluctantORM::Utilities qw(nz check_args);
  1         2  
  1         42  
627 1     1   5 use Class::ReluctantORM::FetchDeep::Results qw(fd_inflate);
  1         2  
  1         45  
628 1     1   4 use Scalar::Util qw(weaken blessed);
  1         2  
  1         44  
629              
630             our $DEBUG = 0;
631 1     1   5 use Data::Dumper;
  1         1  
  1         39  
632              
633 1     1   4 use base 'Class::ReluctantORM::Collection';
  1         2  
  1         2979  
634              
635 0     0     sub rel { return shift->{relationship}; }
636 0     0     sub linking_object { return shift->{linking_object}; }
637              
638             sub _new {
639 0     0     my ($class, %args) = @_;
640 0           foreach my $f (qw(left_class left_key_value right_class join_table join_table_schema) ) {
641 0 0         if (exists $args{$f}) { Class::ReluctantORM::Exception::Call::Deprecated->croak("May not use param $f for Collection::ManyToMany::_new in 0.4 code"); }
  0            
642             }
643 0           foreach my $f (qw(relationship linking_object)) {
644 0 0         unless (exists $args{$f}) { Class::ReluctantORM::Exception::Param::Missing->croak(param => $f); }
  0            
645             }
646              
647 0           my $self = bless \%args, $class;
648 0           weaken($self->{linking_object});
649              
650 0           $self->{_attach_queue} = [];
651 0           $self->{_remove_queue} = [];
652              
653 0 0         if ($args{children}) {
654 0           $self->{_children} = $args{children};
655 0           $self->{_populated} = 1;
656 0           $self->{_count} = scalar @{$args{children}};
  0            
657             } else {
658 0           $self->{_populated} = 0;
659 0           $self->{_count} = undef;
660 0           $self->{_children} = [];
661             }
662              
663 0           return $self;
664             }
665              
666             sub _check_correct_child_class {
667 0     0     my ($self, $object) = @_;
668 0 0         unless (defined($object)) {
669 0           Class::ReluctantORM::Exception::Param::Missing->croak(param => 'object', value => undef, error => "Cannot add an undef entry to a Has-Many-Many collection", frames => 2);
670             }
671 0 0         unless ($object->isa($self->rel->linked_class)) {
672 0           Class::ReluctantORM::Exception::Data::WrongType->croak(param => 'object', expected => $self->rel->linked_class, frames => 2);
673             }
674             }
675              
676             sub all_items {
677 0     0     my $self = shift;
678 0 0         if ($self->is_populated) {
679 0           return @{$self->{_children}};
  0            
680             } else {
681 0           Class::ReluctantORM::Exception::Data::FetchRequired->croak(called => 'all_items', call_instead => 'fetch_all', fetch_locations => [ $self->linking_object->all_origin_traces ]);
682             }
683             }
684              
685 0     0     sub all { goto &all_items; }
686              
687              
688 0     0     sub is_populated { return shift->{_populated}; }
689             sub depopulate {
690 0     0     my $self = shift;
691 0           $self->{_populated} = 0;
692 0           $self->{_count} = undef;
693 0           $self->{_children} = [];
694             }
695              
696             sub count {
697 0     0     my $self = shift;
698 0 0 0       if ($self->is_populated || defined($self->{_count})) {
699 0           return $self->{_count};
700             } else {
701 0           Class::ReluctantORM::Exception::Data::FetchRequired->croak(called => 'count', call_instead => 'fetch_count', fetch_locations => [ $self->linking_object->all_origin_traces ]);
702             }
703             }
704              
705             sub __make_link_where {
706              
707             # TODO - this is duplicate code with make_link_crit
708              
709 0     0     my $collection = shift;
710 0   0       my $use_alias_macro = shift || 0;
711 0           my $rel = $collection->rel;
712              
713 0           my @where = ();
714 0           my @execargs = ();
715              
716             # Create criteria with the join local keys as cols and the local keys as params
717 0           my @local_key_cols = $rel->local_key_columns();
718 0           my @join_local_key_cols = $rel->join_local_key_columns();
719              
720 0           foreach my $index (0..$#local_key_cols) {
721 0           my $join_local_column_name = $join_local_key_cols[$index];
722 0           my $local_field = $rel->linking_class->field_name($local_key_cols[$index]);
723              
724 0           my $crit;
725 0 0         if ($use_alias_macro) {
726 0           $crit = 'MACRO__parent__' . $rel->method_name() . '__.' . $join_local_column_name . ' = ?';
727             } else {
728 0           $crit = $join_local_column_name . ' = ?';
729             }
730              
731 0           push @where, $crit;
732 0           push @execargs, $collection->linking_object->raw_field_value($local_field);
733             }
734 0           return (where => (join ' AND ', @where), execargs => \@execargs);
735             }
736              
737             sub __make_link_crit {
738 0     0     my $collection = shift;
739 0           my $use_alias_macro = shift;
740 0           my $rel = $collection->rel();
741 0           my $linking_obj =$collection->linking_object();
742 0           my $linking_class = $rel->linking_class();
743              
744             # Create criteria with the join local keys as cols and the local keys as params
745 0           my @local_key_cols = $rel->local_key_sql_columns();
746 0           my @join_local_key_cols = $rel->join_local_key_sql_columns();
747              
748 0           my $where = Where->new(
749             Criterion->new(
750             '=',
751             $join_local_key_cols[0],
752             Param->new($linking_obj->raw_field_value($linking_class->field_name($local_key_cols[0]->column))),
753             )
754             );
755              
756 0           foreach my $index (1..$#local_key_cols) {
757 0           my $crit = Criterion->new(
758             '=',
759             $join_local_key_cols[$index],
760             Param->new($linking_obj->raw_field_value($linking_class->field_name($local_key_cols[$index]->column))));
761 0           $where->and($crit);
762             }
763              
764 0           return $where->root_criterion();
765             }
766              
767             =for devnotes
768              
769             =head2 $collection->_set_contents(@children);
770              
771             Assuming you know what you are doing, this method replaces the in-memory guts of the collection. The populated flag is set to true, and the count is set to the new count, but keys are not updated, dirtiness is not changed, and no db activity occurs.
772              
773             =cut
774              
775             sub _set_contents {
776 0     0     my $self = shift;
777 0           my @children = @_;
778              
779 0           $self->{_children} = \@children;
780 0           $self->{_populated} = 1;
781 0           $self->{_count} = scalar @children;
782              
783             }
784              
785             sub fetch_count {
786 0     0     my $collection = shift;
787 0           my $rel = $collection->rel();
788 0           my $parent_obj = $collection->linking_object();
789              
790 0           my $sql = SQL->new('SELECT');
791 0           $sql->from(From->new($rel->join_sql_table));
792 0           $sql->where(Where->new($collection->__make_link_crit(0)));
793 0           my $column = ($rel->join_remote_key_sql_columns)[0];
794 0           my $output = OutputColumn->new
795             (expression => FunctionCall->new('COUNT', $column), alias => 'hmm_count');
796 0           $sql->add_output($output);
797              
798 0           $parent_obj->driver->run_sql($sql);
799 0           $collection->{_count} = $output->output_value();
800 0           return $collection->count();
801             }
802              
803             sub __remote_join_crit {
804 0     0     my $coll = shift;
805 0           my $rel = $coll->rel();
806              
807 0           my @jrc = $rel->join_remote_key_sql_columns();
808 0           my @rc = $rel->remote_key_sql_columns();
809              
810 0           my $crit;
811 0           foreach my $idx (0..$#rc) {
812 0           my $this_crit =
813             Criterion->new(
814             '=',
815             $jrc[$idx],
816             $rc[$idx],
817             );
818 0 0         $crit = $crit ? Criterion->new('AND', $crit, $this_crit) : $this_crit;
819             }
820              
821 0           return $crit;
822              
823             }
824              
825             sub __where_crit_on_join {
826 0     0     my $coll = shift;
827 0           my $rel = $coll->rel();
828              
829 0           my $obj = $coll->linking_object();
830              
831 0           my @jlc = $rel->join_local_key_sql_columns();
832 0           my @pkf = $obj->primary_key_fields();
833              
834 0           my $crit;
835 0           foreach my $idx (0..$#jlc) {
836 0           my $this_crit =
837             Criterion->new(
838             '=',
839             $jlc[$idx],
840             Param->new($obj->raw_field_value($pkf[$idx])),
841             );
842 0 0         $crit = $crit ? Criterion->new('AND', $crit, $this_crit) : $this_crit;
843             }
844 0           return $crit;
845             }
846              
847             sub fetch_all {
848 0     0     my $coll = shift;
849 0           my $rel = $coll->rel();
850              
851 0           my $sql = SQL->new('SELECT');
852 0           my $join = Join->new(
853             'INNER',
854             $rel->remote_sql_table(),
855             $rel->join_sql_table(),
856             $coll->__remote_join_crit(),
857             );
858 0           $join->relationship($rel);
859 0           $sql->from(From->new($join));
860 0           $sql->where(Where->new($coll->__where_crit_on_join()));
861              
862 0           $sql->make_inflatable();
863              
864 0           my @children = fd_inflate($sql);
865 0           $coll->linking_object->capture_origin();
866              
867 0           $coll->{_children} = \@children;
868 0           $coll->{_populated} = 1;
869 0           $coll->{_count} = scalar @children;
870 0           return @children;
871             }
872              
873             sub fetch_deep {
874 0     0     my $self = shift;
875 0           my %args = check_args
876             (
877             args => \@_,
878             required => [ qw(with) ], # As of CRO 0.5, no where, limit, or ordering permitted
879             );
880              
881              
882             # Rely on fetch_deep in parent
883             # By "refetching" the parent
884 0           my %where_args = $self->__make_link_where();
885 0           my $method_name = $self->rel->method_name;
886 0           my $parent = $self->rel->linking_class->fetch_deep(
887             %where_args,
888             with => { $method_name => $args{with} },
889             );
890 0           my @children = $parent->$method_name->all();
891 0           $self->linking_object->capture_origin();
892              
893 0           $self->{_children} = \@children;
894 0           $self->{_populated} = 1;
895 0           $self->{_count} = scalar @children;
896 0           return @children;
897             }
898              
899             =head2 $collection->attach($child);
900              
901             Attach the child object to the parent in memory. Unlike HasMany,
902             HasManyMany does not detach it from any other collections based
903             on this relationship.
904              
905             Both the parent and the child must already be inserted in the database. This operation
906             adds to an internal list of pairings to be inserted into the join
907             table later. Use $collection->commit_pending_attachments() to
908             send the changes to the database.
909              
910             If the collection is populated, the count will be updated.
911              
912             The child will not become dirty. No database activity occurs. To attach
913             and immediately commit the change, use $collection->add().
914              
915             =cut
916              
917             sub attach {
918 0     0     my ($collection, $child, $allow_uninserted_parent) = @_;
919 0           $collection->_check_able_to_attach($child, $allow_uninserted_parent);
920 0           push @{$collection->{_attach_queue}}, $child;
  0            
921 0           $collection->__attach_bidirectional_in_memory($child);
922             }
923              
924             sub _check_able_to_attach {
925 0     0     my ($collection, $child, $allow_uninserted_parent) = @_;
926 0           $collection->_check_correct_child_class($child);
927 0 0         unless ($child->is_inserted()) {
928 0           Class::ReluctantORM::Exception::Data::UnsupportedCascade->croak("Related object must be already inserted in the DB to be attached to a HasManyMany relationship");
929             }
930 0 0         unless ($allow_uninserted_parent) {
931 0 0         unless ($collection->linking_object->is_inserted()) {
932 0           Class::ReluctantORM::Exception::Data::UnsupportedCascade->croak("Parent object must be already inserted in the DB to be attached to a HasManyMany relationship");
933             }
934             }
935 0 0         if ($collection->is_populated()) {
936 0 0         if (grep { $_->id eq $child->id() } $collection->all()) {
  0            
937 0           Class::ReluctantORM::Exception::Data::UniquenessViolation->croak("The child with ID " . $child->id() . " appears to already exist in the " . $collection->rel->method_name() . " relation");
938             }
939             }
940             }
941              
942             =head2 $collection->commit_pending_attachments();
943              
944             Inserts any pending rows into the join table. Call this once after calling attach() repeatedly. It is hoped that database drivers will be able to optimize this into one INSERT, though it may be as many INSERTs as there are rows to insert.
945              
946             =cut
947              
948             sub commit_pending_attachments {
949 0     0     my $collection = shift;
950 0           my @pending = @{$collection->{_attach_queue}};
  0            
951 0 0         unless (@pending) { return; }
  0            
952              
953             # TODO OPTIMIZE With most DB drivers should be possible to send a VALUES table, allowing this to be done in one INSERT
954 0           my $rel = $collection->rel;
955 0           my $sql = $rel->__make_insert_sql();
956              
957 0           my $driver = $collection->linking_object->driver();
958 0           foreach my $child (@pending) {
959 0           my @binds = $rel->__make_join_binds($collection->linking_object(), $child);
960 0           $sql->set_bind_values(@binds);
961 0           $driver->run_sql($sql); # TODO OPTIMIZE add prepare-execute
962             }
963              
964 0           $collection->{_attach_queue} = [];
965 0           return 1;
966             }
967              
968              
969              
970              
971             =head2 $collection->add($child);
972              
973             Inserts a row in the join table linking the parent object and the child object.
974              
975             Unlike HasMany, HasManyMany does not remove the child from any other collections.
976              
977             Calling add() directly does not affect the attach queue - in other words, if you
978             call attach($child1) then add($child2), $child1 will still not be committed. Neither
979             the parent nor the child is becomes dirty during this operation.
980              
981             Note that if you are adding many children, it is more efficient to call attach()
982             repeatedly, then call commit_pending_attachments().
983              
984             =cut
985              
986             sub add {
987 0     0     my ($collection, $child) = @_;
988 0           $collection->_check_able_to_attach($child);
989              
990 0           my $rel = $collection->rel;
991 0           my $sql = $rel->__make_insert_sql();
992              
993 0           my $driver = $collection->linking_object->driver();
994 0           my @binds = $rel->__make_join_binds($collection->linking_object(), $child);
995 0           $sql->set_bind_values(@binds);
996 0           $driver->run_sql($sql);
997              
998 0           $collection->__attach_bidirectional_in_memory($child);
999             }
1000              
1001             sub __attach_in_memory {
1002 0     0     my ($coll, $child) = @_;
1003 0 0         if ($coll->is_populated()) {
1004 0           push @{$coll->{_children}}, $child;
  0            
1005 0           $coll->{_count}++;
1006             }
1007              
1008             }
1009              
1010             sub __attach_bidirectional_in_memory {
1011 0     0     my ($local_coll, $child) = @_;
1012 0           $local_coll->__attach_in_memory($child);
1013              
1014 0           my $inv_rel = $local_coll->rel->inverse_relationship();
1015 0 0         if ($inv_rel) {
1016 0           my $inv_method = $inv_rel->method_name();
1017 0           my $inv_coll = $child->$inv_method;
1018 0           $inv_coll->__attach_in_memory($local_coll->linking_object());
1019             }
1020             }
1021              
1022              
1023             =head2 $collection->remove($child);
1024              
1025             Removes the child from the collection in memory, and removes
1026             the parent from the inverse collection if available. No database activity occurs.
1027              
1028             The child is then placed in a removal queue in the collection. Call
1029             commit_pending_removals() to delete the associations from the join table.
1030              
1031             To delete from memory and DB at once, use delete(). To delete using a SQL query, use delete_where().
1032              
1033             =cut
1034              
1035             sub remove {
1036 0     0     my ($collection, $child) = @_;
1037 0           $collection->_check_able_to_remove($child);
1038 0           push @{$collection->{_remove_queue}}, $child;
  0            
1039 0           $collection->__remove_bidirectional_in_memory($child);
1040             }
1041              
1042              
1043             =head2 $collection->commit_pending_removals();
1044              
1045             Deletes any pending rows into the join table. Call this once after calling remove() repeatedly. It is hoped that database drivers will be able to optimize this into one DELETE.
1046              
1047             =cut
1048              
1049             sub commit_pending_removals {
1050 0     0     my $collection = shift;
1051 0           my @pending = @{$collection->{_remove_queue}};
  0            
1052 0 0         unless (@pending) { return; }
  0            
1053              
1054             # TODO OPTIMIZE With most DB drivers should be possible to send a VALUES table, allowing this to be done in one DELETE
1055 0           my $rel = $collection->rel;
1056 0           my $sql = $rel->__make_delete_sql();
1057              
1058 0           my $driver = $collection->linking_object->driver();
1059 0           foreach my $child (@pending) {
1060 0           my @binds = $rel->__make_join_binds($collection->linking_object(), $child);
1061 0           $sql->set_bind_values(@binds);
1062 0           $driver->run_sql($sql); # TODO OPTIMIZE add prepare-execute
1063             }
1064              
1065 0           $collection->{_delete_queue} = [];
1066 0           return 1;
1067             }
1068              
1069              
1070              
1071              
1072             =head2 $collection->delete($child);
1073              
1074             Deletes all rows, if any, in the join table linking the parent object and the child object.
1075              
1076             Unlike HasMany, HasManyMany does not remove the child from any other collections.
1077              
1078             Calling delete() directly does not affect the removal queue - in other words, if you
1079             call remove($child1) then delete($child2), $child1 will still not be deleted in the database. Neither
1080             the parent nor the child is becomes dirty during this operation.
1081              
1082             Note that if you are removing many children, it is more efficient to call remove()
1083             repeatedly, then call commit_pending_attachments(); alternatively, use SQL and call delete_where (note that delete_where() depopulates the collection, whereas commit_pending_removals() does not.
1084              
1085             =cut
1086              
1087             sub delete {
1088 0     0     my ($collection, $child) = @_;
1089 0           $collection->_check_able_to_remove($child);
1090              
1091 0           my $rel = $collection->rel;
1092 0           my $sql = $rel->__make_delete_sql();
1093              
1094 0           my $driver = $collection->linking_object->driver();
1095 0           my @binds = $rel->__make_join_binds($collection->linking_object(), $child);
1096 0           $sql->set_bind_values(@binds);
1097 0           $driver->run_sql($sql);
1098              
1099 0           $collection->__remove_bidirectional_in_memory($child);
1100             }
1101              
1102             sub _check_able_to_remove {
1103 0     0     my ($collection, $child) = @_;
1104 0           $collection->_check_correct_child_class($child);
1105 0 0         unless ($child->is_inserted()) {
1106 0           Class::ReluctantORM::Exception::Data::UnsupportedCascade->croak("Related object must be already inserted in the DB to be attached to a HasManyMany relationship");
1107             }
1108 0 0         unless ($collection->linking_object->is_inserted()) {
1109 0           Class::ReluctantORM::Exception::Data::UnsupportedCascade->croak("Parent object must be already inserted in the DB to be attached to a HasManyMany relationship");
1110             }
1111 0 0         unless ($collection->is_populated()) {
1112 0           Class::ReluctantORM::Exception::Data::FetchRequired->croak(called => 'delete', call_instead => 'fetch_all or delete_where', fetch_locations => [ $collection->linking_object->all_origin_traces ]);
1113             }
1114             }
1115              
1116             sub __remove_in_memory {
1117 0     0     my ($coll, $child) = @_;
1118 0 0         if ($coll->is_populated()) {
1119 0           $coll->{_children} = [ grep { $_->id ne $child->id } @{$coll->{_children}} ];
  0            
  0            
1120 0           $coll->{_count} = @{$coll->{_children}};
  0            
1121             }
1122             }
1123              
1124             sub __remove_bidirectional_in_memory {
1125 0     0     my ($local_coll, $child) = @_;
1126 0           $local_coll->__remove_in_memory($child);
1127              
1128 0           my $inv_rel = $local_coll->rel->inverse_relationship();
1129 0 0         if ($inv_rel) {
1130 0           my $inv_method = $inv_rel->method_name();
1131 0           my $inv_coll = $child->$inv_method;
1132 0           $inv_coll->__remove_in_memory($local_coll->linking_object());
1133             }
1134             }
1135              
1136             =head2 $collection->delete_where(where => $str, execargs => \@args);
1137              
1138             =head2 $collection->delete_where(where => $where_obj);
1139              
1140             Executes a DELETE against the join table using the provided WHERE clause. A set of criteria is added to the WHERE clause
1141             ensuring that only records associated with the parent object are deleted.
1142              
1143             The where argusment may be either a SQL string or a SQL::Where object.
1144              
1145             =cut
1146              
1147             sub delete_where {
1148 0     0     my $collection = shift;
1149 0 0         if (@_ == 1) { @_ = (where => $_[0]); }
  0            
1150 0 0         if (@_ % 2) { Class::ReluctantORM::Exception::Param::ExpectedHash->croak(); }
  0            
1151 0           my %args = @_;
1152 0 0         unless (defined $args{where}) { Class::ReluctantORM::Exception::Param::Missing->croak(param => 'where'); }
  0            
1153              
1154 0           my $remote_where;
1155 0 0 0       if (Scalar::Util::blessed($args{where}) && $args{where}->isa(Where())) {
1156 0           $remote_where = $args{where};
1157             } else {
1158 0           my $driver = $collection->rel->linked_class->driver();
1159 0           $remote_where = $driver->parse_where($args{where});
1160 0           $remote_where->bind_params(@{$args{execargs}});
  0            
1161             }
1162              
1163             # Strategy: Delete from join table with a 2-part where clause:
1164             # 1. check for remote key in a squbquery using the provided where clause
1165             # 2. Criteria to restrict delete to record associated with the parent
1166 0           my $rel = $collection->rel();
1167 0           my $subselect_statement = SQL->new('SELECT');
1168 0           $subselect_statement->from(From->new($rel->remote_sql_table));
1169 0           $subselect_statement->where($remote_where);
1170 0           $subselect_statement->add_output
1171             (FunctionCall->new('KEY_COMPOSITOR_INSIDE_SUBQUERY',
1172             $rel->remote_key_sql_columns()));
1173 0           my $subquery = SubQuery->new($subselect_statement);
1174 0           my $join_key_check = FunctionCall->new(
1175             'KEY_COMPOSITOR_OUTSIDE_SUBQUERY',
1176             $rel->join_remote_key_sql_columns(),
1177             );
1178 0           my $subquery_crit = Criterion->new('IN',$join_key_check, $subquery);
1179              
1180              
1181 0           my $link_crit = $collection->__make_link_crit(0);
1182 0           my $where = Where->new(
1183             Criterion->new('AND',
1184             $subquery_crit,
1185             $link_crit,
1186             )
1187             );
1188              
1189 0           my $sql = SQL->new('DELETE');
1190 0           $sql->table($collection->rel->join_sql_table());
1191 0           $sql->where($where);
1192              
1193 0           $collection->linking_object->driver->run_sql($sql);
1194 0           $collection->depopulate();
1195              
1196 0           return;
1197             }
1198              
1199             =head2 $collection->delete_all();
1200              
1201             Deletes all associations from the collection.
1202              
1203             =cut
1204              
1205             sub delete_all {
1206 0     0     my $coll = shift;
1207 0           $coll->delete_where(where => Where->new());
1208             }
1209              
1210              
1211             1;