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