1 package DBIx::Class::Schema::Loader::RelBuilder;
6 use Carp::Clan qw/^DBIx::Class/;
7 use Lingua::EN::Inflect::Number ();
9 our $VERSION = '0.05003';
13 DBIx::Class::Schema::Loader::RelBuilder - Builds relationships for DBIx::Class::Schema::Loader
17 See L<DBIx::Class::Schema::Loader>
21 This class builds relationships for L<DBIx::Class::Schema::Loader>. This
22 is module is not (yet) for external use.
28 Arguments: schema_class (scalar), inflect_plural, inflect_singular
30 C<$schema_class> should be a schema class name, where the source
31 classes have already been set up and registered. Column info, primary
32 key, and unique constraints will be drawn from this schema for all
33 of the existing source monikers.
35 Options inflect_plural and inflect_singular are optional, and are better documented
36 in L<DBIx::Class::Schema::Loader::Base>.
40 Arguments: local_moniker (scalar), fk_info (arrayref)
42 This generates the code for the relationships of a given table.
44 C<local_moniker> is the moniker name of the table which had the REFERENCES
45 statements. The fk_info arrayref's contents should take the form:
49 local_columns => [ 'col2', 'col3' ],
50 remote_columns => [ 'col5', 'col7' ],
51 remote_moniker => 'AnotherTableMoniker',
54 local_columns => [ 'col1', 'col4' ],
55 remote_columns => [ 'col1', 'col2' ],
56 remote_moniker => 'YetAnotherTableMoniker',
61 This method will return the generated relationships as a hashref keyed on the
62 class names. The values are arrayrefs of hashes containing method name and
66 'Some::Source::Class' => [
67 { method => 'belongs_to', arguments => [ 'col1', 'Another::Source::Class' ],
68 { method => 'has_many', arguments => [ 'anothers', 'Yet::Another::Source::Class', 'col15' ],
70 'Another::Source::Class' => [
80 my ( $class, $schema, $inflect_pl, $inflect_singular, $rel_attrs ) = @_;
84 inflect_plural => $inflect_pl,
85 inflect_singular => $inflect_singular,
86 relationship_attrs => $rel_attrs,
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";
95 return bless $self => $class;
99 # pluralize a relationship name
100 sub _inflect_plural {
101 my ($self, $relname) = @_;
103 return '' if !defined $relname || $relname eq '';
105 if( ref $self->{inflect_plural} eq 'HASH' ) {
106 return $self->{inflect_plural}->{$relname}
107 if exists $self->{inflect_plural}->{$relname};
109 elsif( ref $self->{inflect_plural} eq 'CODE' ) {
110 my $inflected = $self->{inflect_plural}->($relname);
111 return $inflected if $inflected;
114 return Lingua::EN::Inflect::Number::to_PL($relname);
117 # Singularize a relationship name
118 sub _inflect_singular {
119 my ($self, $relname) = @_;
121 return '' if !defined $relname || $relname eq '';
123 if( ref $self->{inflect_singular} eq 'HASH' ) {
124 return $self->{inflect_singular}->{$relname}
125 if exists $self->{inflect_singular}->{$relname};
127 elsif( ref $self->{inflect_singular} eq 'CODE' ) {
128 my $inflected = $self->{inflect_singular}->($relname);
129 return $inflected if $inflected;
132 return Lingua::EN::Inflect::Number::to_S($relname);
135 # accessor for options to be passed to each generated relationship
136 # type. take single argument, the relationship type name, and returns
137 # either a hashref (if some options are set), or nothing
138 sub _relationship_attrs {
139 my ( $self, $reltype ) = @_;
140 my $r = $self->{relationship_attrs};
141 return unless $r && ( $r->{all} || $r->{$reltype} );
143 my %composite = %{ $r->{all} || {} };
144 if( my $specific = $r->{$reltype} ) {
145 while( my ($k,$v) = each %$specific ) {
155 return unless @$a == @$b;
157 for (my $i = 0; $i < @$a; $i++) {
158 return unless $a->[$i] eq $b->[$i];
164 my ($self, $local_moniker, $local_cols) = @_;
166 # get our base set of attrs from _relationship_attrs, if present
167 my $attrs = $self->_relationship_attrs('belongs_to') || {};
169 # If the referring column is nullable, make 'belongs_to' an
170 # outer join, unless explicitly set by relationship_attrs
171 my $nullable = grep { $self->{schema}->source($local_moniker)->column_info($_)->{is_nullable} }
173 $attrs->{join_type} = 'LEFT'
174 if $nullable && !defined $attrs->{join_type};
179 sub _remote_relname {
180 my ($self, $remote_table, $cond) = @_;
183 # for single-column case, set the remote relname to the column
184 # name, to make filter accessors work, but strip trailing _id
185 if(scalar keys %{$cond} == 1) {
186 my ($col) = values %{$cond};
189 $remote_relname = $self->_inflect_singular($col);
192 $remote_relname = $self->_inflect_singular(lc $remote_table);
195 return $remote_relname;
199 my ($self, $local_moniker, $rels, $uniqs) = @_;
203 my $local_class = $self->{schema}->class($local_moniker);
206 foreach my $rel (@$rels) {
207 next if !$rel->{remote_source};
208 $counters{$rel->{remote_source}}++;
211 foreach my $rel (@$rels) {
212 my $remote_moniker = $rel->{remote_source}
215 my $remote_class = $self->{schema}->class($remote_moniker);
216 my $remote_obj = $self->{schema}->source($remote_moniker);
217 my $remote_cols = $rel->{remote_columns} || [ $remote_obj->primary_columns ];
219 my $local_cols = $rel->{local_columns};
221 if($#$local_cols != $#$remote_cols) {
222 croak "Column count mismatch: $local_moniker (@$local_cols) "
223 . "$remote_moniker (@$remote_cols)";
227 foreach my $i (0 .. $#$local_cols) {
228 $cond{$remote_cols->[$i]} = $local_cols->[$i];
231 my ( $local_relname, $remote_relname, $remote_method ) =
232 $self->_relnames_and_method( $local_moniker, $rel, \%cond, $uniqs, \%counters );
234 push(@{$all_code->{$local_class}},
235 { method => 'belongs_to',
236 args => [ $remote_relname,
239 $self->_remote_attrs($local_moniker, $local_cols),
244 my %rev_cond = reverse %cond;
245 for (keys %rev_cond) {
246 $rev_cond{"foreign.$_"} = "self.".$rev_cond{$_};
247 delete $rev_cond{$_};
250 push(@{$all_code->{$remote_class}},
251 { method => $remote_method,
252 args => [ $local_relname,
255 $self->_relationship_attrs($remote_method),
264 sub _relnames_and_method {
265 my ( $self, $local_moniker, $rel, $cond, $uniqs, $counters ) = @_;
267 my $remote_moniker = $rel->{remote_source};
268 my $remote_obj = $self->{schema}->source( $remote_moniker );
269 my $remote_class = $self->{schema}->class( $remote_moniker );
270 my $remote_relname = lc $self->_remote_relname( $remote_obj->from, $cond);
272 my $local_cols = $rel->{local_columns};
273 my $local_table = $self->{schema}->source($local_moniker)->from;
275 # If more than one rel between this pair of tables, use the local
276 # col names to distinguish
278 my $old_multirel_name; #< TODO: remove me
279 if ( $counters->{$remote_moniker} > 1) {
280 my $colnames = lc(q{_} . join(q{_}, @$local_cols));
281 $remote_relname .= $colnames if keys %$cond > 1;
283 $local_relname = lc($local_table) . $colnames;
284 $local_relname =~ s/_id$//
286 and $old_multirel_name = $self->_inflect_plural( lc($local_table) . $colnames );
287 $local_relname = $self->_inflect_plural( $local_relname );
290 $local_relname = $self->_inflect_plural(lc $local_table);
293 my $remote_method = 'has_many';
295 # If the local columns have a UNIQUE constraint, this is a one-to-one rel
296 my $local_source = $self->{schema}->source($local_moniker);
297 if (_array_eq([ $local_source->primary_columns ], $local_cols) ||
298 grep { _array_eq($_->[1], $local_cols) } @$uniqs) {
299 $remote_method = 'might_have';
300 $local_relname = $self->_inflect_singular($local_relname);
302 $old_multirel_name = $self->_inflect_singular($old_multirel_name);
305 # TODO: remove me after 0.05003 release
307 and warn __PACKAGE__." $VERSION: warning, stripping trailing _id from ${remote_class} relation '$old_multirel_name', renaming to '$local_relname'. This behavior is new as of 0.05003.\n";
309 return ( $local_relname, $remote_relname, $remote_method );
314 See L<DBIx::Class::Schema::Loader/AUTHOR> and L<DBIx::Class::Schema::Loader/CONTRIBUTORS>.
318 This library is free software; you can redistribute it and/or modify it under
319 the same terms as Perl itself.