_id
my $column = $self->class_to_table_singular . '_id';
return [ $column ] if($meta->column($column));
# 3. The first serial column in the column list, alphabetically
foreach my $column (sort { lc $a->name cmp lc $b->name } $meta->columns)
{
return [ $column->name ] if($column->type =~ /^(?:big)?serial$/);
}
# 4. The first column
if(my $column = $meta->first_column)
{
return [ $column->name ];
}
return;
}
sub auto_column_method_name
{
my($self, $type, $column, $name, $object_class) = @_;
return lc $name if ($self->force_lowercase);
return undef; # rely on hard-coded defaults in Metadata
}
sub init_singular_to_plural_function { }
sub init_plural_to_singular_function { }
sub singular_to_plural
{
my($self, $word) = @_;
if(my $code = $self->singular_to_plural_function)
{
return $code->($word);
}
if($word =~ /(?:x|[se]s)$/i)
{
return $word . 'es';
}
else
{
$word =~ s/y$/ies/i;
}
return $word =~ /s$/i ? $word : ($word . 's');
}
sub plural_to_singular
{
my($self, $word) = @_;
if(my $code = $self->plural_to_singular_function)
{
return $code->($word);
}
$word =~ s/ies$/y/i;
return $word if($word =~ s/ses$/s/i);
return $word if($word =~ /[aeiouy]ss$/i);
$word =~ s/s$//i;
return $word;
}
sub method_name_conflicts
{
my($self, $name) = @_;
return 1 if(Rose::DB::Object->can($name));
my $meta = $self->meta;
foreach my $column ($meta->columns)
{
foreach my $type ($column->auto_method_types)
{
my $method = $column->method_name($type) ||
$meta->method_name_from_column_name($column->name, $type) ||
next;
return 1 if($name eq $method);
}
}
foreach my $foreign_key ($meta->foreign_keys)
{
foreach my $type ($foreign_key->auto_method_types)
{
my $method =
$foreign_key->method_name($type) ||
$foreign_key->build_method_name_for_type($type) || next;
return 1 if($name eq $method);
}
}
foreach my $relationship ($meta->relationships)
{
foreach my $type ($relationship->auto_method_types)
{
my $method =
$relationship->method_name($type) ||
$relationship->build_method_name_for_type($type) || next;
return 1 if($name eq $method);
}
}
return 0;
}
sub auto_column_sequence_name
{
my($self, $table, $column, $db) = @_;
my $name = join('_', $table, $column, 'seq');
return uc $name if($db && $db->likes_uppercase_sequence_names);
return lc $name if($db && $db->likes_lowercase_sequence_names);
return $name;
}
sub auto_primary_key_column_sequence_name { shift->auto_column_sequence_name(@_) }
sub auto_foreign_key_name
{
my($self, $f_class, $current_name, $key_columns, $used_names) = @_;
if($self->force_lowercase)
{
$current_name = lc $current_name;
$key_columns = { map { lc } %$key_columns };
}
my $f_meta = $f_class->meta or return $current_name;
my $name = $self->plural_to_singular($f_meta->table) || $current_name;
if(keys %$key_columns == 1)
{
my($local_column, $foreign_column) = %$key_columns;
# Try to lop off foreign column name. Example:
# my_foreign_object_id -> my_foreign_object
if($local_column =~ s/_$foreign_column$//i)
{
$name = $local_column;
}
else
{
$name = $self->plural_to_singular($f_meta->table) || $current_name;
}
}
# Avoid method name conflicts
if($self->method_name_conflicts($name) || $used_names->{$name})
{
foreach my $s ('_obj', '_object')
{
# Try the name with a suffix appended
unless($self->method_name_conflicts($name . $s) ||
$used_names->{$name . $s})
{
return $name . $s;
}
}
my $i = 1;
# Give up and go with numbers...
$i++ while($self->method_name_conflicts($name . $i) ||
$used_names->{$name . $i});
return $name . $i;
}
return $name;
}
sub auto_table_to_relationship_name_plural
{
my($self, $table) = @_;
$table = lc $table if ($self->force_lowercase);
return $self->tables_are_singular ? $self->singular_to_plural($table) : $table;
}
sub auto_class_to_relationship_name_plural
{
my($self, $class) = @_;
return $self->class_to_table_plural($class);
}
sub auto_foreign_key_to_relationship_name_plural
{
my($self, $fk) = @_;
my $name = $self->force_lowercase ? lc $fk->name : $fk->name;
return $self->singular_to_plural($name);
}
sub auto_relationship_name_one_to_many
{
my($self, $table, $class) = @_;
#return $self->auto_class_to_relationship_name_plural($class);
my $name = $self->auto_table_to_relationship_name_plural($table);
# Avoid method name conflicts
if($self->method_name_conflicts($name))
{
foreach my $s ('_objs', '_objects')
{
# Try the name with a suffix appended
unless($self->method_name_conflicts($name . $s))
{
return $name . $s;
}
}
my $i = 1;
# Give up and go with numbers...
$i++ while($self->method_name_conflicts($name . $i));
return $name . $i;
}
return $name;
}
sub auto_relationship_name_many_to_many
{
my($self, $fk, $map_class) = @_;
my $name = $self->auto_foreign_key_to_relationship_name_plural($fk);
# Avoid method name conflicts
if($self->method_name_conflicts($name))
{
foreach my $s ('_objs', '_objects')
{
# Try the name with a suffix appended
unless($self->method_name_conflicts($name . $s))
{
return $name . $s;
}
}
my $i = 1;
# Give up and go with numbers...
$i++ while($self->method_name_conflicts($name . $i));
return $name . $i;
}
return $name;
}
sub auto_relationship_name_one_to_one
{
my($self, $table, $class) = @_;
$table = lc $table if ($self->force_lowercase);
my $name = $self->plural_to_singular($table);
# Avoid method name conflicts
if($self->method_name_conflicts($name))
{
foreach my $s ('_obj', '_object')
{
# Try the name with a suffix appended
unless($self->method_name_conflicts($name . $s))
{
return $name . $s;
}
}
my $i = 1;
# Give up and go with numbers...
$i++ while($self->method_name_conflicts($name . $i));
return $name . $i;
}
return $name;
}
sub is_map_class
{
my($self, $class) = @_;
return 0 unless(UNIVERSAL::isa($class, 'Rose::DB::Object'));
my $is_map_table = $self->looks_like_map_table($class->meta->table);
my $is_map_class = $self->looks_like_map_class($class);
return 1 if($is_map_table && (!defined $is_map_class || $is_map_class));
return 0;
}
sub looks_like_map_class
{
my($self, $class) = @_;
unless(UNIVERSAL::isa($class, 'Rose::DB::Object'))
{
return undef;
}
my $meta = $class->meta;
my @fks = $meta->foreign_keys;
return 1 if(@fks == 2);
return 0 if(($meta->is_initialized || $meta->initialized_foreign_keys) &&
!$meta->has_deferred_foreign_keys);
return undef;
}
sub looks_like_map_table
{
my($self, $table) = @_;
if($table =~ m{^(?:
(?:\w+_){2,}map # foo_bar_map
| (?:\w+_)*\w+_(?:\w+_)*\w+s # foo_bars
| (?:\w+_)*\w+s_(?:\w+_)*\w+s # foos_bars
)$}xi)
{
return 1;
}
return 0;
}
sub auto_foreign_key
{
my($self, $name, $spec) = @_;
$spec ||= {};
my $meta = $self->meta;
unless($spec->{'class'})
{
my $class = $meta->class;
my $fk_class = $self->related_table_to_class($name, $class);
LOAD:
{
# Try to load class
no strict 'refs';
unless(UNIVERSAL::isa($fk_class, 'Rose::DB::Object'))
{
local $@;
eval "require $fk_class";
return if($@ || !UNIVERSAL::isa($fk_class, 'Rose::DB::Object'));
}
}
#return unless(UNIVERSAL::isa($fk_class, 'Rose::DB::Object'));
$spec->{'class'} = $fk_class;
}
unless(defined $spec->{'key_columns'})
{
my @fpk_columns = UNIVERSAL::isa($spec->{'class'}, 'Rose::DB::Object') ?
$spec->{'class'}->meta->primary_key_column_names : ();
# Defer population of key columns until the foreign class is initialized
unless(@fpk_columns == 1)
{
# If the foreign class has more than one primary key column, give up
return if(@fpk_columns);
# If the foreign class is initialized and the foreign key spec still
# has no key columns, then give up.
if(UNIVERSAL::isa($spec->{'class'}, 'Rose::DB::Object') &&
$spec->{'class'}->meta->is_initialized)
{
return;
}
my %spec = %$spec;
$meta->add_deferred_task(
{
class => $meta->class,
method => "foreign_key:$name",
code => sub
{
# Generate new foreign key, then grab the key columns from it
my $new_fk = $self->auto_foreign_key($name, \%spec) or return;
my $fk = $meta->foreign_key($name);
my $key_cols = $new_fk->key_columns or return;
$fk->key_columns($key_cols);
},
check => sub
{
my $fk = $meta->foreign_key($name) or return 0;
# If the foreign class is initialized and the foreign key still
# has no key columns, then we should give up.
if(UNIVERSAL::isa($fk->class, 'Rose::DB::Object') &&
$fk->class->meta->is_initialized)
{
Carp::croak "Missing key columns for foreign key named ",
$fk->name, " in class ", $meta->class;
}
my $cols = $fk->key_columns or return 0;
# Everything is okay if we have key columns
return (ref($cols) && keys(%$cols) > 0) ? 1 : 0;
}
});
return Rose::DB::Object::Metadata::ForeignKey->new(name => $name, %$spec);
}
my $aliases = $meta->column_aliases;
if($meta->column($name) && $aliases->{$name} && $aliases->{$name} ne $name)
{
$spec->{'key_columns'} = { $name => $fpk_columns[0] };
}
elsif($meta->column("${name}_$fpk_columns[0]"))
{
$spec->{'key_columns'} = { "${name}_$fpk_columns[0]" => $fpk_columns[0] };
}
else { return }
}
return Rose::DB::Object::Metadata::ForeignKey->new(name => $name, %$spec);
}
sub auto_relationship
{
my($self, $name, $rel_class, $spec) = @_;
$spec ||= {};
my $meta = $self->meta;
my $rel_type = $rel_class->type;
unless($spec->{'class'})
{
if($rel_type eq 'one to many')
{
my $class = $meta->class;
# Get class suffix from relationship name
my $table = $self->plural_to_singular($name);
my $f_class = $self->related_table_to_class($table, $class);
LOAD:
{
# Try to load class
no strict 'refs';
unless(UNIVERSAL::isa($f_class, 'Rose::DB::Object'))
{
local $@;
eval "require $f_class";
return if($@ || !UNIVERSAL::isa($f_class, 'Rose::DB::Object'));
}
}
#return unless(UNIVERSAL::isa($f_class, 'Rose::DB::Object'));
$spec->{'class'} = $f_class;
}
elsif($rel_type =~ /^(?:one|many) to one$/)
{
my $class = $meta->class;
# Get class suffix from relationship name
my $f_class = $self->related_table_to_class($name, $class);
LOAD:
{
# Try to load class
no strict 'refs';
unless(UNIVERSAL::isa($f_class, 'Rose::DB::Object'))
{
local $@;
eval "require $f_class";
return if($@ || !UNIVERSAL::isa($f_class, 'Rose::DB::Object'));
}
}
#return unless(UNIVERSAL::isa($f_class, 'Rose::DB::Object'));
$spec->{'class'} = $f_class;
}
}
# Make sure this class has its @ISA set up...
unless(UNIVERSAL::isa($spec->{'class'}, 'Rose::DB::Object'))
{
# ...but allow many-to-many relationships to pass because they tend to
# need more time before every piece of info is available.
return unless($rel_type eq 'many to many');
}
if($rel_type eq 'one to one')
{
return $self->auto_relationship_one_to_one($name, $rel_class, $spec);
}
elsif($rel_type eq 'many to one')
{
return $self->auto_relationship_many_to_one($name, $rel_class, $spec);
}
elsif($rel_type eq 'one to many')
{
return $self->auto_relationship_one_to_many($name, $rel_class, $spec);
}
elsif($rel_type eq 'many to many')
{
return $self->auto_relationship_many_to_many($name, $rel_class, $spec);
}
return;
}
sub auto_relationship_one_to_one
{
my($self, $name, $rel_class, $spec) = @_;
$spec ||= {};
my $meta = $self->meta;
unless(defined $spec->{'column_map'})
{
my @fpk_columns = $spec->{'class'}->meta->primary_key_column_names;
return unless(@fpk_columns == 1);
my $aliases = $meta->column_aliases;
if($meta->column($name) && $aliases->{$name} && $aliases->{$name} ne $name)
{
$spec->{'column_map'} = { $name => $fpk_columns[0] };
}
elsif($meta->column("${name}_$fpk_columns[0]"))
{
$spec->{'column_map'} = { "${name}_$fpk_columns[0]" => $fpk_columns[0] };
}
elsif($meta->column("${name}_id"))
{
$spec->{'column_map'} = { "${name}_id" => $fpk_columns[0] };
}
else { return }
}
return $rel_class->new(name => $name, %$spec);
}
*auto_relationship_many_to_one = \&auto_relationship_one_to_one;
sub auto_relationship_one_to_many
{
my($self, $name, $rel_class, $spec) = @_;
$spec ||= {};
my $meta = $self->meta;
my $l_col_name = $self->class_to_table_singular;
unless(defined $spec->{'column_map'})
{
my @pk_columns = $meta->primary_key_column_names;
return unless(@pk_columns == 1);
my @fpk_columns = $meta->primary_key_column_names;
return unless(@fpk_columns == 1);
my $f_meta = $spec->{'class'}->meta;
my $aliases = $f_meta->column_aliases;
if($f_meta->column($l_col_name))
{
$spec->{'column_map'} = { $pk_columns[0] => $l_col_name };
}
elsif($f_meta->column("${l_col_name}_$pk_columns[0]"))
{
$spec->{'column_map'} = { $pk_columns[0] => "${l_col_name}_$pk_columns[0]" };
}
else { return }
}
return $rel_class->new(name => $name, %$spec);
}
sub auto_relationship_many_to_many
{
my($self, $name, $rel_class, $spec) = @_;
$spec ||= {};
my $meta = $self->meta;
unless($spec->{'map_class'})
{
my $class = $meta->class;
# Given:
# Class: My::Object
# Rel name: other_objects
# Foreign class: My::OtherObject
#
# Consider map class names:
# My::ObjectsOtherObjectsMap
# My::ObjectOtherObjectMap
# My::OtherObjectsObjectsMap
# My::OtherObjectObjectMap
# My::ObjectsOtherObjects
# My::ObjectOtherObjects
# My::OtherObjectsObjects
# My::OtherObjectObjects
# My::OtherObjectMap
# My::OtherObjectsMap
# My::ObjectMap
# My::ObjectsMap
my $prefix = $self->class_prefix($class);
my @consider;
my $f_class_suffix = $self->table_to_class($name);
my $f_class_suffix_pl = $self->table_to_class_plural($name);
$class =~ /(\w+)$/;
my $class_suffix = $1;
my $class_suffix_pl = $self->singular_to_plural($class_suffix);
push(@consider, map { "${prefix}$_" }
$class_suffix_pl . $f_class_suffix_pl . 'Map',
$class_suffix . $f_class_suffix . 'Map',
$f_class_suffix_pl . $class_suffix_pl . 'Map',
$f_class_suffix . $class_suffix . 'Map',
$class_suffix_pl . $f_class_suffix_pl,
$class_suffix . $f_class_suffix_pl,
$f_class_suffix_pl . $class_suffix_pl,
$f_class_suffix . $class_suffix_pl,
$f_class_suffix . 'Map',
$f_class_suffix_pl . 'Map',
$class_suffix . 'Map',
$class_suffix_pl . 'Map');
my $map_class;
CLASS: foreach my $class (@consider)
{
LOAD:
{
# Try to load class
no strict 'refs';
if(UNIVERSAL::isa($class, 'Rose::DB::Object'))
{
$map_class = $class;
last CLASS;
}
else
{
local $@;
eval "require $class";
unless($@)
{
$map_class = $class;
last CLASS if(UNIVERSAL::isa($class, 'Rose::DB::Object'));
}
}
}
}
return unless($map_class && UNIVERSAL::isa($map_class, 'Rose::DB::Object'));
$spec->{'map_class'} = $map_class;
}
return $rel_class->new(name => $name, %$spec);
}
1;
__END__
=head1 NAME
Rose::DB::Object::ConventionManager - Provide missing metadata by convention.
=head1 SYNOPSIS
package My::Product;
use base 'Rose::DB::Object';
__PACKAGE__->meta->setup(columns => [ ... ]);
# No table is set above, but look at this: the
# convention manager provided one for us.
print __PACKAGE__->meta->table; # "products"
##
## See the EXAMPLE section below for a more complete demonstration.
##
=head1 DESCRIPTION
Each L-derived object has a L that it uses to fill in missing L. The convention manager encapsulates a set of rules (conventions) for generating various pieces of metadata in the absence of explicitly specified values: table names, column names, etc.
Each L-derived class's convention manager object is stored in the L attribute of its L (L) object. L is the default convention manager class.
The object method documentation below describes both the purpose of each convention manager method and the particular rules that L follows to fulfill that purpose. Subclasses must honor the purpose of each method, but are free to use any rules they choose.
B When reading the descriptions of the rules used by each convention manager method below, remember that only values that are I will be set by the convention manager. Explicitly providing a value for a piece of metadata obviates the need for the convention manager to generate one.
If insufficient information is available, or if the convention manager simply declines to fulfill a request, undef may be returned from any metadata-generating method.
In the documentation, the adjectives "local" and "foreign" are used to distinguish between the things that belong to the convention manager's L and the class on "the other side" of the inter-table relationship, respectively.
=head1 SUMMARY OF DEFAULT CONVENTIONS
Although the object method documentation below includes all the information required to understand the default conventions, it's also quite spread out. What follows is a summary of the default conventions. Some details have necessarily been omitted or simplified for the sake of brevity, but this summary should give you a good starting point for further exploration.
Here's a brief summary of the default conventions as implemented in L.
=over 4
=item B
Examples: C, C, C, C.
=item B
Examples: C, C, C, C, C.
(This convention can be overridden via the L method.)
=item B
Examples: C, C, C, C, C.
=item B
For example, the primary key column name in the C table might be C or C, but should B be C or C.
=item B
Examples: C, C, C.
=item B
Examples: C, C, C. These relationships may point to zero or one foreign object. The default method names generated from such relationships are based on the relationship names, so singular names make the most sense.
=item B
Examples: C, C, C. These relationships may point to more than one foreign object. The default method names generated from such relationships are based on the relationship names, so plural names make the most sense.
=item B
See the L, L, and L documentation for all the details.
=back
=head1 CONSTRUCTOR
=over 4
=item B
Constructs a new object based on PARAMS, where PARAMS are
name/value pairs. Any object attribute is a valid parameter name.
=back
=head1 OBJECT METHODS
=over 4
=item B
Given a L column L, a L object or column name, a default method name, and a L-derived class name, return an appropriate method name. The default implementation simply returns undef, relying on the hard-coded default method-type-to-name mapping implemented in L's L method.
=item B
Given a L name and an optional reference to a hash SPEC of the type passed to L's L method, return an appropriately constructed L object.
The foreign key's L is generated by calling L, passing NAME and the convention manager's L as arguments. An attempt is made is load the class. If this fails, the foreign key's L is not set.
The foreign key's L are only set if both the "local" and "foreign" tables have single-column primary keys. The foreign class's primary key column name is used as the foreign column in the L map. If there is a local column with the same name as the foreign key name, and if that column is aliased (making way for the foreign key method to use that name), then that is used as the local column. If not, then the local column name is generated by joining the foreign key name and the foreign class's primary key column name with an underscore. If no column by that name exists, then the search is abandoned. Example:
Given these pieces:
Name Description Value
--------- -------------------------------- -------
NAME Foreign key name vendor
FCLASS Foreign class My::Vendor
FPK Foreign primary key column name id
Consider column maps in this order:
Value Formula
--------------------- ----------------------
{ vendor => 'id' } { NAME => FPK }
{ vendor_id => 'id' } { _ => FPK }
=item B
Given the name of a foreign class, the current foreign key name (if any), a reference to a hash of L, and a reference to a hash whose keys are foreign key names already used in this class, return a L for the foreign key.
If there is more than one pair of columns in KEY_COLUMNS, then the name is generated by calling L, passing the L
name of the foreign class. The CURRENT_NAME is used if the call to L does not return a true value.
If there is just one pair of columns in KEY_COLUMNS, and if the name of the local column ends with an underscore and the name of the referenced column, then that part of the column name is removed and the remaining string is used as the foreign key name. For example, given the following tables:
CREATE TABLE categories
(
id SERIAL PRIMARY KEY,
...
);
CREATE TABLE products
(
category_id INT REFERENCES categories (id),
...
);
The foreign key name would be "category", which is the name of the referring column ("category_id") with an underscore and the name of the referenced column ("_id") removed from the end of it.
If the foreign key has only one column, but it does not meet the criteria described above, then the name is generated by calling L, passing the L
name of the foreign class. The CURRENT_NAME is used if the call to L does not return a true value.
If the name selected using the above techniques is in the USED_NAMES hash, or is the same as that of an existing or potential method in the target class, then the suffixes "_obj" and "_object" are tried in that order. If neither of those suffixes resolves the situation, then ascending numeric suffixes starting with "1" are tried until a unique name is found.
=item B
Given a table name and the name of the L-derived class that fronts it, return a base name suitable for use as the value of the C parameter to L's L method.
If no table is specified then the table name is derived from the current class
name by calling L.
If L is true, then TABLE is passed to the L method and the result is returned. Otherwise, TABLE is returned as-is.
=item B
Return the class that all manager classes will default to inheriting from. By
default this will be L.
=item B
Given the name of a L-derived class, returns a class name for a L-derived class to manage such objects. The default implementation simply appends "::Manager" to the L-derived class name.
=item B
Given the specified L L,
L, and L