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; |