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   9370 use strict;
  15         35  
  15         470  
3 15     15   78 use warnings;
  15         31  
  15         441  
4 15     15   88 use parent "DBIx::DataModel::Meta";
  15         29  
  15         83  
5 15     15   926 use DBIx::DataModel;
  15         34  
  15         88  
6 15     15   94 use DBIx::DataModel::Meta::Utils qw/define_method define_readonly_accessors/;
  15         31  
  15         918  
7              
8 15     15   103 use Carp::Clan qw[^(DBIx::DataModel::|SQL::Abstract)];
  15         31  
  15         119  
9 15     15   1438 use Params::Validate qw/validate_with SCALAR ARRAYREF HASHREF OBJECT UNDEF/;
  15         41  
  15         1247  
10 15     15   116 use List::MoreUtils qw/pairwise/;
  15         40  
  15         207  
11 15     15   10573 use Scalar::Util qw/weaken dualvar looks_like_number/;
  15         33  
  15         1110  
12 15     15   115 use Module::Load qw/load/;
  15         39  
  15         130  
13 15     15   6947 use POSIX qw/LONG_MAX/;
  15         84253  
  15         110  
14 15     15   16190 use namespace::clean;
  15         33  
  15         104  
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 80 my $class = shift;
42              
43 32         791 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         815 for my $letter (qw/A B/) {
51             # parse parameters for this association end
52 64         110 my @letter_params = %{$self->{$letter}};
  64         273  
53 64         1455 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     387 if $assoc_end->{join_cols} && !@{$assoc_end->{join_cols}};
  12         50  
61              
62             # transform multiplicity scalar into a pair [$min, $max]
63 64         212 $class->_parse_multiplicity($assoc_end);
64              
65 64         219 $self->{$letter} = $assoc_end;
66             }
67              
68             # set default association name
69 32 100       81 my @names = map {$self->{$_}{role} || $self->{$_}{table}{name}} qw/A B/;
  64         277  
70 32   33     237 $self->{name} ||= join "_", @names;
71              
72             # if many-to-many, needs special treatment
73 32         55 my $install_method;
74 32 100 100     183 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         63 $install_method = '_install_path';
81              
82             # handle implicit column names
83 30 100       152 if ($self->{A}{multiplicity}[1] > 1) { # n-to-1
    100          
84 2   33     6 $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     157 $self->{A}{join_cols} ||= $self->{A}{table}{primary_key};
89 26   33     115 $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       48 @{$self->{A}{join_cols}} == @{$self->{B}{join_cols}}
  30         96  
  30         80  
94             or croak "Association: numbers of columns do not match";
95             }
96              
97             # instantiate
98 32         76 bless $self, $class;
99              
100             # special checks for compositions
101 32 100       186 $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     115 or croak "at least one side of the association must have a role name";
106 32 100       178 $self->$install_method(qw/A B/) if $self->{B}{role};
107 32 50       194 $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         71 delete@{$self}{qw/A B/};
  32         228  
111              
112             # avoid circular reference
113 32         162 weaken $self->{schema};
114              
115 32         118 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   151 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       401 $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         300 my $max_is_star = !looks_like_number($2);
145 64 100       264 my $min = defined $1 ? $1 : ($max_is_star ? 0 : $2);
    100          
146 64 100       201 my $max = $max_is_star ? dualvar(POSIX::LONG_MAX, '*') : $2;
147 64         221 $assoc_end->{multiplicity} = [$min, $max];
148             }
149              
150              
151             sub _install_many_to_many {
152 4     4   11 my ($self, $from, $to) = @_;
153              
154             # path must contain exactly 2 items (intermediate table + remote table)
155 4         31 my $role = $self->{$to}{role};
156 4         11 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         22 $self->{$from}{table}->define_navigation_method($role, @path);
162             }
163              
164              
165             sub _install_path {
166 58     58   149 my ($self, $from, $to) = @_;
167              
168             # build the "ON" condition for SQL::Abstract::More
169 58         124 my $from_cols = $self->{$from}{join_cols};
170 58         125 my $to_cols = $self->{$to} {join_cols};
171 58     58   556 my %condition = pairwise {$a => $b} @$from_cols, @$to_cols;
  58         306  
172              
173             # define path
174 58         264 my $path_metaclass = $self->{schema}{path_metaclass};
175 58         265 load $path_metaclass;
176 58         3050 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         301 association => $self,
184             direction => "$from$to",
185             );
186              
187             # if 1-to-many, define insertion method
188 58 100       245 if ($self->{$to}{multiplicity}[1] > 1) {
189              
190             # build method parts
191 28         77 my $method_name = "insert_into_$path_name";
192 28         65 my $to_table_name = $self->{$to}{table}{name};
193             my $method_body = sub {
194 6     6   21828 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         20 foreach my $record (@_) {
199              
200             # if this is a scalar, it's no longer a record, but an arg to insert()
201 12 100       32 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       21 not (grep {exists $record->{$_}} @$to_cols) or
  10         55  
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         44 $record = {%$record};
209 10         23 @{$record}{@$to_cols} = @{$source}{@$from_cols};
  10         26  
  10         46  
210             }
211              
212 6         23 return $source->schema->table($to_table_name)->insert(@_);
213 28         221 };
214              
215             # define the method
216             define_method(
217             class => $self->{$from}{table}{class},
218 28         219 name => $method_name,
219             body => $method_body,
220             );
221             }
222             }
223              
224             sub _check_composition {
225 14     14   32 my $self = shift;
226              
227             # multiplicities must be 1-to-n
228 14 50       49 $self->{A}{multiplicity}[1] == 1
229             or croak "max multiplicity of first class in a composition must be 1";
230 14 50       51 $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       38 while (my ($name, $path) = each %{$self->{B}{table}{path} || {}}) {
  14         160  
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__