File Coverage

blib/lib/DBIx/DataModel/Meta/Association.pm
Criterion Covered Total %
statement 113 115 98.2
branch 33 48 68.7
condition 11 30 36.6
subroutine 19 19 100.0
pod 1 1 100.0
total 177 213 83.1


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