add is_deferrable => 1 to belongs_to by default
[dbsrgits/DBIx-Class-Schema-Loader.git] / lib / DBIx / Class / Schema / Loader / RelBuilder.pm
CommitLineData
996be9ee 1package DBIx::Class::Schema::Loader::RelBuilder;
2
3use strict;
4use warnings;
7824616e 5use Class::C3;
fa994d3c 6use Carp::Clan qw/^DBIx::Class/;
996be9ee 7use Lingua::EN::Inflect::Number ();
39b22ca9 8use Lingua::EN::Inflect::Phrase ();
996be9ee 9
e42ec4ef 10our $VERSION = '0.05003';
32f784fc 11
996be9ee 12=head1 NAME
13
14DBIx::Class::Schema::Loader::RelBuilder - Builds relationships for DBIx::Class::Schema::Loader
15
16=head1 SYNOPSIS
17
18See L<DBIx::Class::Schema::Loader>
19
20=head1 DESCRIPTION
21
22This class builds relationships for L<DBIx::Class::Schema::Loader>. This
23is module is not (yet) for external use.
24
25=head1 METHODS
26
27=head2 new
28
e8ad6491 29Arguments: schema_class (scalar), inflect_plural, inflect_singular
996be9ee 30
31C<$schema_class> should be a schema class name, where the source
32classes have already been set up and registered. Column info, primary
33key, and unique constraints will be drawn from this schema for all
34of the existing source monikers.
35
996be9ee 36Options inflect_plural and inflect_singular are optional, and are better documented
37in L<DBIx::Class::Schema::Loader::Base>.
38
39=head2 generate_code
40
e8ad6491 41Arguments: local_moniker (scalar), fk_info (arrayref)
42
43This generates the code for the relationships of a given table.
44
45C<local_moniker> is the moniker name of the table which had the REFERENCES
46statements. The fk_info arrayref's contents should take the form:
47
48 [
49 {
50 local_columns => [ 'col2', 'col3' ],
51 remote_columns => [ 'col5', 'col7' ],
52 remote_moniker => 'AnotherTableMoniker',
53 },
54 {
55 local_columns => [ 'col1', 'col4' ],
56 remote_columns => [ 'col1', 'col2' ],
57 remote_moniker => 'YetAnotherTableMoniker',
58 },
59 # ...
60 ],
61
62This method will return the generated relationships as a hashref keyed on the
63class names. The values are arrayrefs of hashes containing method name and
64arguments, like so:
996be9ee 65
66 {
67 'Some::Source::Class' => [
b97c2c1e 68 { method => 'belongs_to', arguments => [ 'col1', 'Another::Source::Class' ],
69 { method => 'has_many', arguments => [ 'anothers', 'Yet::Another::Source::Class', 'col15' ],
996be9ee 70 ],
71 'Another::Source::Class' => [
72 # ...
73 ],
74 # ...
75 }
8f9d7ce5 76
996be9ee 77=cut
78
79sub new {
c8c27020 80
81 my ( $class, $schema, $inflect_pl, $inflect_singular, $rel_attrs ) = @_;
996be9ee 82
83 my $self = {
84 schema => $schema,
996be9ee 85 inflect_plural => $inflect_pl,
86 inflect_singular => $inflect_singular,
c8c27020 87 relationship_attrs => $rel_attrs,
996be9ee 88 };
89
c8c27020 90 # validate the relationship_attrs arg
91 if( defined $self->{relationship_attrs} ) {
92 ref($self->{relationship_attrs}) eq 'HASH'
93 or croak "relationship_attrs must be a hashref";
94 }
996be9ee 95
c8c27020 96 return bless $self => $class;
996be9ee 97}
98
99
100# pluralize a relationship name
101sub _inflect_plural {
c496748b 102 my ($self, $relname, $method) = @_;
996be9ee 103
39ef3bfe 104 return '' if !defined $relname || $relname eq '';
105
996be9ee 106 if( ref $self->{inflect_plural} eq 'HASH' ) {
107 return $self->{inflect_plural}->{$relname}
108 if exists $self->{inflect_plural}->{$relname};
109 }
110 elsif( ref $self->{inflect_plural} eq 'CODE' ) {
111 my $inflected = $self->{inflect_plural}->($relname);
112 return $inflected if $inflected;
113 }
114
c496748b 115 $method ||= '_to_PL';
116
117 return $self->$method($relname);
996be9ee 118}
119
120# Singularize a relationship name
121sub _inflect_singular {
ee07e280 122 my ($self, $relname, $method) = @_;
996be9ee 123
39ef3bfe 124 return '' if !defined $relname || $relname eq '';
125
996be9ee 126 if( ref $self->{inflect_singular} eq 'HASH' ) {
127 return $self->{inflect_singular}->{$relname}
128 if exists $self->{inflect_singular}->{$relname};
129 }
130 elsif( ref $self->{inflect_singular} eq 'CODE' ) {
131 my $inflected = $self->{inflect_singular}->($relname);
132 return $inflected if $inflected;
133 }
134
ee07e280 135 $method ||= '_to_S';
136
137 return $self->$method($relname);
c496748b 138}
139
140sub _to_PL {
141 my ($self, $name) = @_;
142
143 $name =~ s/_/ /g;
39b22ca9 144 my $plural = Lingua::EN::Inflect::Phrase::to_PL($name);
c496748b 145 $plural =~ s/ /_/g;
146
147 return $plural;
148}
149
150sub _old_to_PL {
151 my ($self, $name) = @_;
152
153 return Lingua::EN::Inflect::Number::to_PL($name);
154}
155
156sub _to_S {
157 my ($self, $name) = @_;
158
39b22ca9 159 $name =~ s/_/ /g;
160 my $singular = Lingua::EN::Inflect::Phrase::to_S($name);
161 $singular =~ s/ /_/g;
162
163 return $singular;
996be9ee 164}
165
ee07e280 166sub _old_to_S {
167 my ($self, $name) = @_;
168
169 return Lingua::EN::Inflect::Number::to_S($name);
170}
171
53ef681d 172sub _default_relationship_attrs { +{
173 has_many => {
174 cascade_delete => 0,
175 cascade_copy => 0,
176 },
177 might_have => {
178 cascade_delete => 0,
179 cascade_copy => 0,
180 },
181 belongs_to => {
182 on_delete => 'CASCADE',
183 on_update => 'CASCADE',
ee07e280 184 is_deferrable => 1,
53ef681d 185 },
186} }
187
c8c27020 188# accessor for options to be passed to each generated relationship
189# type. take single argument, the relationship type name, and returns
190# either a hashref (if some options are set), or nothing
191sub _relationship_attrs {
192 my ( $self, $reltype ) = @_;
193 my $r = $self->{relationship_attrs};
c8c27020 194
53ef681d 195 my %composite = (
196 %{ $self->_default_relationship_attrs->{$reltype} || {} },
197 %{ $r->{all} || {} }
198 );
199
c8c27020 200 if( my $specific = $r->{$reltype} ) {
201 while( my ($k,$v) = each %$specific ) {
202 $composite{$k} = $v;
203 }
204 }
205 return \%composite;
206}
207
26f1c8c9 208sub _array_eq {
209 my ($a, $b) = @_;
210
211 return unless @$a == @$b;
212
213 for (my $i = 0; $i < @$a; $i++) {
214 return unless $a->[$i] eq $b->[$i];
215 }
216 return 1;
217}
218
c39e403e 219sub _remote_attrs {
c496748b 220 my ($self, $local_moniker, $local_cols) = @_;
c39e403e 221
c496748b 222 # get our base set of attrs from _relationship_attrs, if present
223 my $attrs = $self->_relationship_attrs('belongs_to') || {};
c8c27020 224
c496748b 225 # If the referring column is nullable, make 'belongs_to' an
226 # outer join, unless explicitly set by relationship_attrs
227 my $nullable = grep { $self->{schema}->source($local_moniker)->column_info($_)->{is_nullable} } @$local_cols;
228 $attrs->{join_type} = 'LEFT' if $nullable && !defined $attrs->{join_type};
c39e403e 229
c496748b 230 return $attrs;
c39e403e 231}
232
f2fc8d01 233sub _remote_relname {
234 my ($self, $remote_table, $cond) = @_;
235
236 my $remote_relname;
237 # for single-column case, set the remote relname to the column
238 # name, to make filter accessors work, but strip trailing _id
239 if(scalar keys %{$cond} == 1) {
240 my ($col) = values %{$cond};
243c6ebc 241 $col = lc $col;
f2fc8d01 242 $col =~ s/_id$//;
243 $remote_relname = $self->_inflect_singular($col);
244 }
245 else {
246 $remote_relname = $self->_inflect_singular(lc $remote_table);
247 }
248
249 return $remote_relname;
250}
251
996be9ee 252sub generate_code {
26f1c8c9 253 my ($self, $local_moniker, $rels, $uniqs) = @_;
996be9ee 254
255 my $all_code = {};
256
e8ad6491 257 my $local_class = $self->{schema}->class($local_moniker);
057fbb08 258
e8ad6491 259 my %counters;
260 foreach my $rel (@$rels) {
261 next if !$rel->{remote_source};
262 $counters{$rel->{remote_source}}++;
263 }
264
265 foreach my $rel (@$rels) {
057fbb08 266 my $remote_moniker = $rel->{remote_source}
267 or next;
268
269 my $remote_class = $self->{schema}->class($remote_moniker);
270 my $remote_obj = $self->{schema}->source($remote_moniker);
271 my $remote_cols = $rel->{remote_columns} || [ $remote_obj->primary_columns ];
272
273 my $local_cols = $rel->{local_columns};
e8ad6491 274
275 if($#$local_cols != $#$remote_cols) {
276 croak "Column count mismatch: $local_moniker (@$local_cols) "
277 . "$remote_moniker (@$remote_cols)";
996be9ee 278 }
279
e8ad6491 280 my %cond;
281 foreach my $i (0 .. $#$local_cols) {
282 $cond{$remote_cols->[$i]} = $local_cols->[$i];
283 }
996be9ee 284
057fbb08 285 my ( $local_relname, $remote_relname, $remote_method ) =
39ef3bfe 286 $self->_relnames_and_method( $local_moniker, $rel, \%cond, $uniqs, \%counters );
7dba7c70 287
e8ad6491 288 push(@{$all_code->{$local_class}},
289 { method => 'belongs_to',
290 args => [ $remote_relname,
291 $remote_class,
292 \%cond,
c39e403e 293 $self->_remote_attrs($local_moniker, $local_cols),
e8ad6491 294 ],
996be9ee 295 }
e8ad6491 296 );
297
057fbb08 298 my %rev_cond = reverse %cond;
299 for (keys %rev_cond) {
300 $rev_cond{"foreign.$_"} = "self.".$rev_cond{$_};
301 delete $rev_cond{$_};
302 }
303
e8ad6491 304 push(@{$all_code->{$remote_class}},
26f1c8c9 305 { method => $remote_method,
e8ad6491 306 args => [ $local_relname,
307 $local_class,
308 \%rev_cond,
c8c27020 309 $self->_relationship_attrs($remote_method),
e8ad6491 310 ],
311 }
312 );
996be9ee 313 }
314
315 return $all_code;
316}
317
39ef3bfe 318sub _relnames_and_method {
057fbb08 319 my ( $self, $local_moniker, $rel, $cond, $uniqs, $counters ) = @_;
e9c09ed9 320
057fbb08 321 my $remote_moniker = $rel->{remote_source};
322 my $remote_obj = $self->{schema}->source( $remote_moniker );
323 my $remote_class = $self->{schema}->class( $remote_moniker );
243c6ebc 324 my $remote_relname = lc $self->_remote_relname( $remote_obj->from, $cond);
fa6f8d4e 325
057fbb08 326 my $local_cols = $rel->{local_columns};
327 my $local_table = $self->{schema}->source($local_moniker)->from;
328
329 # If more than one rel between this pair of tables, use the local
330 # col names to distinguish
c496748b 331 my ($local_relname, $old_local_relname, $local_relname_uninflected, $old_local_relname_uninflected);
057fbb08 332 if ( $counters->{$remote_moniker} > 1) {
243c6ebc 333 my $colnames = lc(q{_} . join(q{_}, @$local_cols));
057fbb08 334 $remote_relname .= $colnames if keys %$cond > 1;
335
ff098bf3 336 $local_relname = lc($local_table) . $colnames;
c496748b 337 $local_relname =~ s/_id$//;
338
339 $local_relname_uninflected = $local_relname;
057fbb08 340 $local_relname = $self->_inflect_plural( $local_relname );
341
c496748b 342 $old_local_relname_uninflected = lc($local_table) . $colnames;
343 $old_local_relname = $self->_inflect_plural( lc($local_table) . $colnames, '_old_to_PL' );
344
057fbb08 345 } else {
c496748b 346 $local_relname_uninflected = lc $local_table;
057fbb08 347 $local_relname = $self->_inflect_plural(lc $local_table);
c496748b 348
349 $old_local_relname_uninflected = lc $local_table;
350 $old_local_relname = $self->_inflect_plural(lc $local_table, '_old_to_PL');
057fbb08 351 }
fa6f8d4e 352
057fbb08 353 my $remote_method = 'has_many';
354
355 # If the local columns have a UNIQUE constraint, this is a one-to-one rel
356 my $local_source = $self->{schema}->source($local_moniker);
357 if (_array_eq([ $local_source->primary_columns ], $local_cols) ||
358 grep { _array_eq($_->[1], $local_cols) } @$uniqs) {
359 $remote_method = 'might_have';
c496748b 360 $local_relname = $self->_inflect_singular($local_relname_uninflected);
ee07e280 361 $old_local_relname = $self->_inflect_singular($old_local_relname_uninflected, '_old_to_S');
057fbb08 362 }
fa6f8d4e 363
c496748b 364 warn __PACKAGE__." $VERSION: renaming ${remote_class} relation '$old_local_relname' to '$local_relname'. This behavior is new as of 0.05003.\n" if $old_local_relname && $local_relname ne $old_local_relname;
fa6f8d4e 365
057fbb08 366 return ( $local_relname, $remote_relname, $remote_method );
fa6f8d4e 367}
368
be80bba7 369=head1 AUTHOR
370
9cc8e7e1 371See L<DBIx::Class::Schema::Loader/AUTHOR> and L<DBIx::Class::Schema::Loader/CONTRIBUTORS>.
be80bba7 372
373=head1 LICENSE
374
375This library is free software; you can redistribute it and/or modify it under
376the same terms as Perl itself.
377
378=cut
379
996be9ee 3801;