Moved inflation to inflate_result in Row.pm
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Relationship / Base.pm
1 package DBIx::Class::Relationship::Base;
2
3 use strict;
4 use warnings;
5
6 use base qw/DBIx::Class/;
7
8 __PACKAGE__->mk_classdata('_relationships', { } );
9
10 =head1 NAME 
11
12 DBIx::Class::Relationship::Base - Inter-table relationships
13
14 =head1 SYNOPSIS
15
16 =head1 DESCRIPTION
17
18 This class handles relationships between the tables in your database
19 model. It allows your to set up relationships, and to perform joins
20 on searches.
21
22 =head1 METHODS
23
24 =head2 add_relationship
25
26   __PACKAGE__->add_relationship('relname', 'Foreign::Class', $cond, $attrs);
27
28 The condition needs to be an SQL::Abstract-style representation of the
29 join between the tables. For example, if you're creating a rel from Foo to Bar,
30
31   { 'foreign.foo_id' => 'self.id' }
32
33 will result in the JOIN clause
34
35   foo me JOIN bar bar ON bar.foo_id = me.id
36
37 You can specify as many foreign => self mappings as necessary.
38
39 Valid attributes are as follows:
40
41 =over 4
42
43 =item join_type
44
45 Explicitly specifies the type of join to use in the relationship. Any SQL
46 join type is valid, e.g. C<LEFT> or C<RIGHT>. It will be placed in the SQL
47 command immediately before C<JOIN>.
48
49 =item proxy
50
51 An arrayref containing a list of accessors in the foreign class to proxy in
52 the main class. If, for example, you do the following:
53   
54   __PACKAGE__->might_have(bar => 'Bar', undef, { proxy => qw[/ margle /] });
55   
56 Then, assuming Bar has an accessor named margle, you can do:
57
58   my $obj = Foo->find(1);
59   $obj->margle(10); # set margle; Bar object is created if it doesn't exist
60   
61 =item accessor
62
63 Specifies the type of accessor that should be created for the relationship.
64 Valid values are C<single> (for when there is only a single related object),
65 C<multi> (when there can be many), and C<filter> (for when there is a single
66 related object, but you also want the relationship accessor to double as
67 a column accessor). For C<multi> accessors, an add_to_* method is also
68 created, which calls C<create_related> for the relationship.
69
70 =back
71
72 =cut
73
74 sub add_relationship {
75   my ($class, $rel, $f_class, $cond, $attrs) = @_;
76   die "Can't create relationship without join condition" unless $cond;
77   $attrs ||= {};
78   eval "require $f_class;";
79   if ($@) {
80     $class->throw($@) unless $@ =~ /Can't locate/;
81   }
82   my %rels = %{ $class->_relationships };
83   $rels{$rel} = { class => $f_class,
84                   cond  => $cond,
85                   attrs => $attrs };
86   $class->_relationships(\%rels);
87
88   return unless eval { $f_class->can('columns'); }; # Foreign class not loaded
89   eval { $class->_resolve_join($rel, 'me') };
90
91   if ($@) { # If the resolve failed, back out and re-throw the error
92     delete $rels{$rel}; # 
93     $class->_relationships(\%rels);
94     $class->throw("Error creating relationship $rel: $@");
95   }
96   1;
97 }
98
99 sub _resolve_join {
100   my ($class, $join, $alias) = @_;
101   if (ref $join eq 'ARRAY') {
102     return map { $class->_resolve_join($_, $alias) } @$join;
103   } elsif (ref $join eq 'HASH') {
104     return map { $class->_resolve_join($_, $alias),
105                  $class->_relationships->{$_}{class}->_resolve_join($join->{$_}, $_) }
106            keys %$join;
107   } elsif (ref $join) {
108     $class->throw("No idea how to resolve join reftype ".ref $join);
109   } else {
110     my $rel_obj = $class->_relationships->{$join};
111     $class->throw("No such relationship ${join}") unless $rel_obj;
112     my $j_class = $rel_obj->{class};
113     my %join = (_action => 'join',
114          _aliases => { 'self' => $alias, 'foreign' => $join },
115          _classes => { $alias => $class, $join => $j_class });
116     my $j_cond = $j_class->resolve_condition($rel_obj->{cond}, \%join);
117     return [ { $join => $j_class->_table_name,
118                -join_type => $rel_obj->{attrs}{join_type} || '' }, $j_cond ];
119   }
120 }
121
122 sub resolve_condition {
123   my ($self, $cond, $attrs) = @_;
124   if (ref $cond eq 'HASH') {
125     my %ret;
126     foreach my $key (keys %$cond) {
127       my $val = $cond->{$key};
128       if (ref $val) {
129         $self->throw("Can't handle this yet :(");
130       } else {
131         $ret{$self->_cond_key($attrs => $key)}
132           = $self->_cond_value($attrs => $key => $val);
133       }
134     }
135     return \%ret;
136   } else {
137    $self->throw("Can't handle this yet :(");
138   }
139 }
140
141 sub _cond_key {
142   my ($self, $attrs, $key, $alias) = @_;
143   my $action = $attrs->{_action} || '';
144   if ($action eq 'convert') {
145     unless ($key =~ s/^foreign\.//) {
146       $self->throw("Unable to convert relationship to WHERE clause: invalid key ${key}");
147     }
148     if (defined (my $alias = $attrs->{_aliases}{foreign})) {
149       return "${alias}.${key}";
150     } else {
151       return $key;
152     }
153   } elsif ($action eq 'join') {
154     return $key unless $key =~ /\./;
155     my ($type, $field) = split(/\./, $key);
156     if (my $alias = $attrs->{_aliases}{$type}) {
157       my $class = $attrs->{_classes}{$alias};
158       $self->throw("Unknown column $field on $class as $alias")
159         unless $class->has_column($field);
160       return join('.', $alias, $field);
161     } else {
162       $self->throw( "Unable to resolve type ${type}: only have aliases for ".
163             join(', ', keys %{$attrs->{_aliases} || {}}) );
164     }
165   }
166   return $self->next::method($attrs, $key);
167 }
168
169 sub _cond_value {
170   my ($self, $attrs, $key, $value) = @_;
171   my $action = $attrs->{_action} || '';
172   if ($action eq 'convert') {
173     unless ($value =~ s/^self\.//) {
174       $self->throw( "Unable to convert relationship to WHERE clause: invalid value ${value}" );
175     }
176     unless ($self->has_column($value)) {
177       $self->throw( "Unable to convert relationship to WHERE clause: no such accessor ${value}" );
178     }
179     return $self->get_column($value);
180   } elsif ($action eq 'join') {
181     return $key unless $key =~ /\./;
182     my ($type, $field) = split(/\./, $value);
183     if (my $alias = $attrs->{_aliases}{$type}) {
184       my $class = $attrs->{_classes}{$alias};
185       $self->throw("Unknown column $field on $class as $alias")
186         unless $class->has_column($field);
187       return join('.', $alias, $field);
188     } else {
189       $self->throw( "Unable to resolve type ${type}: only have aliases for ".
190             join(', ', keys %{$attrs->{_aliases} || {}}) );
191     }
192   }
193       
194   return $self->next::method($attrs, $key, $value)
195 }
196
197 =head2 search_related
198
199   My::Table->search_related('relname', $cond, $attrs);
200
201 =cut
202
203 sub search_related {
204   my $self = shift;
205   my $rel = shift;
206   my $attrs = { };
207   if (@_ > 1 && ref $_[$#_] eq 'HASH') {
208     $attrs = { %{ pop(@_) } };
209   }
210   my $rel_obj = $self->_relationships->{$rel};
211   $self->throw( "No such relationship ${rel}" ) unless $rel_obj;
212   $attrs = { %{$rel_obj->{attrs} || {}}, %{$attrs || {}} };
213
214   $self->throw( "Invalid query: @_" ) if (@_ > 1 && (@_ % 2 == 1));
215   my $query = ((@_ > 1) ? {@_} : shift);
216
217   $attrs->{_action} = 'convert'; # shouldn't we resolve the cond to something
218                                  # to merge into the AST really?
219   my ($cond) = $self->resolve_condition($rel_obj->{cond}, $attrs);
220   $query = ($query ? { '-and' => [ $cond, $query ] } : $cond);
221   #use Data::Dumper; warn Dumper($query);
222   #warn $rel_obj->{class}." $meth $cond ".join(', ', @{$attrs->{bind}||[]});
223   delete $attrs->{_action};
224   return $self->resolve_class($rel_obj->{class}
225            )->search($query, $attrs);
226 }
227
228 =head2 count_related
229
230   My::Table->count_related('relname', $cond, $attrs);
231
232 =cut
233
234 sub count_related {
235   my $self = shift;
236   return $self->search_related(@_)->count;
237 }
238
239 =head2 create_related
240
241   My::Table->create_related('relname', \%col_data);
242
243 =cut
244
245 sub create_related {
246   my $class = shift;
247   return $class->new_related(@_)->insert;
248 }
249
250 =head2 new_related
251
252   My::Table->new_related('relname', \%col_data);
253
254 =cut
255
256 sub new_related {
257   my ($self, $rel, $values, $attrs) = @_;
258   $self->throw( "Can't call new_related as class method" ) 
259     unless ref $self;
260   $self->throw( "new_related needs a hash" ) 
261     unless (ref $values eq 'HASH');
262   my $rel_obj = $self->_relationships->{$rel};
263   $self->throw( "No such relationship ${rel}" ) unless $rel_obj;
264   $self->throw( "Can't abstract implicit create for ${rel}, condition not a hash" )
265     unless ref $rel_obj->{cond} eq 'HASH';
266   $attrs = { %{$rel_obj->{attrs}}, %{$attrs || {}}, _action => 'convert' };
267
268   my %fields = %{$self->resolve_condition($rel_obj->{cond},$attrs)};
269   $fields{$_} = $values->{$_} for keys %$values;
270
271   return $self->resolve_class($rel_obj->{class})->new(\%fields);
272 }
273
274 =head2 find_related
275
276   My::Table->find_related('relname', @pri_vals | \%pri_vals);
277
278 =cut
279
280 sub find_related {
281   my $self = shift;
282   my $rel = shift;
283   my $rel_obj = $self->_relationships->{$rel};
284   $self->throw( "No such relationship ${rel}" ) unless $rel_obj;
285   my ($cond) = $self->resolve_condition($rel_obj->{cond}, { _action => 'convert' });
286   $self->throw( "Invalid query: @_" ) if (@_ > 1 && (@_ % 2 == 1));
287   my $attrs = { };
288   if (@_ > 1 && ref $_[$#_] eq 'HASH') {
289     $attrs = { %{ pop(@_) } };
290   }
291   my $query = ((@_ > 1) ? {@_} : shift);
292   $query = ($query ? { '-and' => [ $cond, $query ] } : $cond);
293   return $self->resolve_class($rel_obj->{class})->find($query);
294 }
295
296 =head2 find_or_create_related
297
298   My::Table->find_or_create_related('relname', \%col_data);
299
300 =cut
301
302 sub find_or_create_related {
303   my $self = shift;
304   return $self->find_related(@_) || $self->create_related(@_);
305 }
306
307 =head2 set_from_related
308
309   My::Table->set_from_related('relname', $rel_obj);
310
311 =cut
312
313 sub set_from_related {
314   my ($self, $rel, $f_obj) = @_;
315   my $rel_obj = $self->_relationships->{$rel};
316   $self->throw( "No such relationship ${rel}" ) unless $rel_obj;
317   my $cond = $rel_obj->{cond};
318   $self->throw( "set_from_related can only handle a hash condition; the "
319     ."condition for $rel is of type ".(ref $cond ? ref $cond : 'plain scalar'))
320       unless ref $cond eq 'HASH';
321   my $f_class = $self->resolve_class($rel_obj->{class});
322   $self->throw( "Object $f_obj isn't a ".$f_class )
323     unless $f_obj->isa($f_class);
324   foreach my $key (keys %$cond) {
325     next if ref $cond->{$key}; # Skip literals and complex conditions
326     $self->throw("set_from_related can't handle $key as key")
327       unless $key =~ m/^foreign\.([^\.]+)$/;
328     my $val = $f_obj->get_column($1);
329     $self->throw("set_from_related can't handle ".$cond->{$key}." as value")
330       unless $cond->{$key} =~ m/^self\.([^\.]+)$/;
331     $self->set_column($1 => $val);
332   }
333   return 1;
334 }
335
336 =head2 update_from_related
337
338   My::Table->update_from_related('relname', $rel_obj);
339
340 =cut
341
342 sub update_from_related {
343   my $self = shift;
344   $self->set_from_related(@_);
345   $self->update;
346 }
347
348 =head2 delete_related
349
350   My::Table->delete_related('relname', $cond, $attrs);
351
352 =cut
353
354 sub delete_related {
355   my $self = shift;
356   return $self->search_related(@_)->delete;
357 }
358
359 1;
360
361 =head1 AUTHORS
362
363 Matt S. Trout <mst@shadowcatsystems.co.uk>
364
365 =head1 LICENSE
366
367 You may distribute this code under the same terms as Perl itself.
368
369 =cut
370