File Coverage

blib/lib/DBIx/DataModel/Meta/Association.pm
Criterion Covered Total %
statement 117 119 98.3
branch 33 48 68.7
condition 13 33 39.3
subroutine 20 20 100.0
pod 1 1 100.0
total 184 221 83.2


line stmt bran cond sub pod time code
1             package DBIx::DataModel::Meta::Association;
2 16     16   13279 use strict;
  16         38  
  16         646  
3 16     16   83 use warnings;
  16         32  
  16         913  
4 16     16   107 use parent "DBIx::DataModel::Meta";
  16         61  
  16         112  
5 16     16   1141 use DBIx::DataModel;
  16         28  
  16         111  
6 16     16   85 use DBIx::DataModel::Meta::Utils qw/define_method define_readonly_accessors/;
  16         55  
  16         1116  
7 16     16   130 use DBIx::DataModel::Carp;
  16         58  
  16         119  
8             # use Carp::Clan qw(^(DBIx::DataModel|SQL::Abstract));
9              
10              
11              
12 16     16   1002 use Params::Validate qw/validate_with SCALAR ARRAYREF HASHREF OBJECT UNDEF/;
  16         33  
  16         1427  
13 16     16   92 use List::MoreUtils qw/pairwise/;
  16         28  
  16         139  
14 16     16   13381 use Scalar::Util qw/weaken dualvar looks_like_number/;
  16         39  
  16         1382  
15 16     16   111 use Module::Load qw/load/;
  16         46  
  16         130  
16 16     16   7861 use POSIX qw/LONG_MAX/;
  16         90994  
  16         105  
17 16     16   21532 use namespace::clean;
  16         35  
  16         152  
18              
19              
20             # specification for parameters to new()
21             my $association_spec = {
22             schema => {type => OBJECT, isa => "DBIx::DataModel::Meta::Schema"},
23             A => {type => HASHREF},
24             B => {type => HASHREF},
25             name => {type => SCALAR, optional => 1}, # computed if absent
26             kind => {type => SCALAR,
27             regex => qr/^(Association|Aggregation|Composition)$/},
28             };
29              
30             # specification for sub-parameters 'A' and 'B'
31             my $association_end_spec = {
32             table => {type => OBJECT,
33             isa => 'DBIx::DataModel::Meta::Source::Table'},
34             role => {type => SCALAR|UNDEF, optional => 1},
35             multiplicity => {type => SCALAR|ARRAYREF}, # if scalar : "$min..$max"
36             join_cols => {type => ARRAYREF, optional => 1},
37             };
38              
39             #----------------------------------------------------------------------
40             # PUBLIC METHODS
41             #----------------------------------------------------------------------
42              
43             sub new {
44 33     33 1 78 my $class = shift;
45              
46 33         961 my $self = validate_with(
47             params => \@_,
48             spec => $association_spec,
49             allow_extra => 0,
50             );
51              
52             # work on both association ends (A and B)
53 33         932 for my $letter (qw/A B/) {
54             # parse parameters for this association end
55 66         117 my @letter_params = %{$self->{$letter}};
  66         318  
56 66         1549 my $assoc_end = validate_with(
57             params => \@letter_params,
58             spec => $association_end_spec,
59             allow_extra => 0,
60             );
61              
62             croak "join_cols is present but empty"
63 66 50 66     445 if $assoc_end->{join_cols} && !@{$assoc_end->{join_cols}};
  14         68  
64              
65             # transform multiplicity scalar into a pair [$min, $max]
66 66         215 $class->_parse_multiplicity($assoc_end);
67              
68 66         232 $self->{$letter} = $assoc_end;
69             }
70              
71             # set default association name
72 33 100       88 my @names = map {$self->{$_}{role} || $self->{$_}{table}{name}} qw/A B/;
  66         268  
73 33   33     306 $self->{name} ||= join "_", @names;
74              
75             # if many-to-many, needs special treatment
76 33         57 my $install_method;
77 33 100 100     186 if ($self->{A}{multiplicity}[1] > 1 && $self->{B}{multiplicity}[1] > 1) {
78 2         8 $install_method = '_install_many_to_many';
79             }
80              
81             # otherwise, treat as a regular association
82             else {
83 31         59 $install_method = '_install_path';
84              
85             # handle implicit column names
86 31 100       214 if ($self->{A}{multiplicity}[1] > 1) { # n-to-1
    100          
87 3   33     10 $self->{B}{join_cols} ||= $self->{B}{table}{primary_key};
88 3   33     9 $self->{A}{join_cols} ||= $self->{B}{join_cols};
89             }
90             elsif ($self->{B}{multiplicity}[1] > 1) { # 1-to-n
91 26   33     267 $self->{A}{join_cols} ||= $self->{A}{table}{primary_key};
92 26   33     180 $self->{B}{join_cols} ||= $self->{A}{join_cols};
93             }
94              
95             # check if we have the same number of columns on both sides
96 31 50       49 @{$self->{A}{join_cols}} == @{$self->{B}{join_cols}}
  31         69  
  31         122  
97             or croak "Association: numbers of columns do not match";
98             }
99              
100             # instantiate
101 33         82 bless $self, $class;
102              
103             # special checks for compositions
104 33 100       229 $self->_check_composition if $self->{kind} eq 'Composition';
105              
106             # install methods from A to B and B to A, if role names are not empty
107             $self->{A}{role} || $self->{B}{role}
108 33 0 33     117 or croak "at least one side of the association must have a role name";
109 33 100       228 $self->$install_method(qw/A B/) if $self->{B}{role};
110 33 50       180 $self->$install_method(qw/B A/) if $self->{A}{role};
111              
112             # EXPERIMENTAL : no longer need association ends; all info is stored in Paths
113 33         65 delete@{$self}{qw/A B/};
  33         158  
114              
115             # avoid circular reference
116 33         112 weaken $self->{schema};
117              
118 33         193 return $self;
119             }
120              
121              
122             # accessor methods
123             define_readonly_accessors(__PACKAGE__, qw/schema name kind path_AB path_BA/);
124              
125              
126             #----------------------------------------------------------------------
127             # PRIVATE UTILITY METHODS
128             #----------------------------------------------------------------------
129              
130             sub _parse_multiplicity {
131 66     66   154 my ($class, $assoc_end) = @_;
132              
133             # nothing to do if already an arrayref
134 66 50       186 return if ref $assoc_end->{multiplicity};
135              
136             # otherwise, parse the scalar
137 66 50       430 $assoc_end->{multiplicity} =~ /^(?: # optional part
138             (\d+) # minimum
139             \s*\.\.\s* # followed by ".."
140             )? # end of optional part
141             (\d+|\*|n) # maximum
142             $/x
143             or croak "illegal multiplicity : $assoc_end->{multiplicity}";
144              
145             # multiplicity '*' is a shortcut for '0..*', and
146             # multiplicity '1' is a shortcut for '1..1'.
147 66         346 my $max_is_star = !looks_like_number($2);
148 66 100       275 my $min = defined $1 ? $1 : ($max_is_star ? 0 : $2);
    100          
149 66 100       212 my $max = $max_is_star ? dualvar(POSIX::LONG_MAX, '*') : $2;
150 66         283 $assoc_end->{multiplicity} = [$min, $max];
151             }
152              
153              
154             sub _install_many_to_many {
155 4     4   14 my ($self, $from, $to) = @_;
156              
157             # path must contain exactly 2 items (intermediate table + remote table)
158 4         11 my $role = $self->{$to}{role};
159 4         9 my @path = @{$self->{$to}{join_cols}};
  4         16  
160 4 50       16 @path == 2
161             or croak "many-to-many : should have exactly 2 roles";
162              
163             # define the method
164 4         24 $self->{$from}{table}->define_navigation_method($role, @path);
165             }
166              
167              
168             sub _install_path {
169 60     60   147 my ($self, $from, $to) = @_;
170              
171             # build the "ON" condition for SQL::Abstract::More
172 60         176 my $from_cols = $self->{$from}{join_cols};
173 60         137 my $to_cols = $self->{$to} {join_cols};
174 60     60   673 my %condition = pairwise {$a => $b} @$from_cols, @$to_cols;
  60         284  
175              
176             # define path
177 60         314 my $path_metaclass = $self->{schema}{path_metaclass};
178 60         295 load $path_metaclass;
179 60         3341 my $path_name = $self->{$to}{role};
180             $self->{"path_$from$to"} = $path_metaclass->new(
181             name => $path_name,
182             from => $self->{$from}{table},
183             to => $self->{$to}{table},
184             on => \%condition,
185             multiplicity => $self->{$to}{multiplicity},
186 60         389 association => $self,
187             direction => "$from$to",
188             );
189              
190             # if 1-to-many, define insertion method
191 60 100       443 if ($self->{$to}{multiplicity}[1] > 1) {
192              
193             # build method parts
194 29         199 my $method_name = "insert_into_$path_name";
195 29         86 my $to_table_name = $self->{$to}{table}{name};
196             my $method_body = sub {
197 6     6   22346 my $source = shift; # remaining @_ contains refs to records for insert()
198 6 50       17 ref($source) or croak "$method_name cannot be called as class method";
199              
200             # add join information into records that will be inserted
201 6         13 foreach my $record (@_) {
202              
203             # if this is a scalar, it's no longer a record, but an arg to insert()
204 12 100       26 last if !ref $record; # since args are at the end, we exit the loop
205              
206             # check that we won't overwrite existing data
207 10 50       21 not (grep {exists $record->{$_}} @$to_cols) or
  10         32  
208             croak "args to $method_name should not contain values in @$to_cols";
209              
210             # shallow copy and insert values for the join
211 10         34 $record = {%$record};
212 10         19 @{$record}{@$to_cols} = @{$source}{@$from_cols};
  10         17  
  10         22  
213             }
214              
215 6         21 return $source->schema->table($to_table_name)->insert(@_);
216 29         170 };
217              
218             # define the method
219             define_method(
220             class => $self->{$from}{table}{class},
221 29         123 name => $method_name,
222             body => $method_body,
223             );
224             }
225             }
226              
227             sub _check_composition {
228 14     14   35 my $self = shift;
229              
230             # multiplicities must be 1-to-n
231 14         76 my $msg = "Composition([$self->{A}{table}{name} ..], [$self->{B}{table}{name} ..])";
232 14 50       50 $self->_multiplicity_is_exactly_1('A')
233             or croak "$msg: $self->{A}{table}{name} must have multiplicity 1..1";
234 14 50       46 ! $self->_multiplicity_is_exactly_1('B')
235             or croak "$msg: $self->{B}{table}{name} must not have multiplicity 1..1";
236              
237             # check for conflicting compositions
238 14 50       33 while (my ($name, $path) = each %{$self->{B}{table}{path} || {}}) {
  14         225  
239 0 0 0     0 if ($path->association->kind eq 'Composition' && $path->direction eq 'BA'
      0        
240             && ($path->multiplicity)[0] > 0) {
241 0         0 croak "$self->{B}{table}{name} can't be a component "
242             . "of $self->{A}{table}{name} "
243             . "(already component of $path->{to}{name})";
244             }
245             }
246             }
247              
248             sub _multiplicity_is_exactly_1 {
249 28     28   65 my ($self, $end) = @_;
250 28         60 my $mult = $self->{$end}{multiplicity};
251              
252 28   66     189 return $mult->[0] == 1 && $mult->[1] == 1;
253             }
254              
255              
256              
257             1;
258              
259             __END__