DBIx-DataModel
view release on metacpan or search on metacpan
lib/DBIx/DataModel/Meta/Association.pm view on Meta::CPAN
package DBIx::DataModel::Meta::Association;
use strict;
use warnings;
use parent "DBIx::DataModel::Meta";
use DBIx::DataModel;
use DBIx::DataModel::Meta::Utils qw/define_method define_readonly_accessors/;
use DBIx::DataModel::Carp;
# use Carp::Clan qw(^(DBIx::DataModel|SQL::Abstract));
use Params::Validate qw/validate_with SCALAR ARRAYREF HASHREF OBJECT UNDEF/;
use List::MoreUtils qw/pairwise/;
use Scalar::Util qw/weaken dualvar looks_like_number/;
use Module::Load qw/load/;
use POSIX qw/LONG_MAX/;
use namespace::clean;
# specification for parameters to new()
my $association_spec = {
schema => {type => OBJECT, isa => "DBIx::DataModel::Meta::Schema"},
A => {type => HASHREF},
B => {type => HASHREF},
name => {type => SCALAR, optional => 1}, # computed if absent
kind => {type => SCALAR,
regex => qr/^(Association|Aggregation|Composition)$/},
};
# specification for sub-parameters 'A' and 'B'
my $association_end_spec = {
table => {type => OBJECT,
isa => 'DBIx::DataModel::Meta::Source::Table'},
role => {type => SCALAR|UNDEF, optional => 1},
multiplicity => {type => SCALAR|ARRAYREF}, # if scalar : "$min..$max"
join_cols => {type => ARRAYREF, optional => 1},
};
#----------------------------------------------------------------------
# PUBLIC METHODS
#----------------------------------------------------------------------
sub new {
my $class = shift;
my $self = validate_with(
params => \@_,
spec => $association_spec,
allow_extra => 0,
);
# work on both association ends (A and B)
for my $letter (qw/A B/) {
# parse parameters for this association end
my @letter_params = %{$self->{$letter}};
my $assoc_end = validate_with(
params => \@letter_params,
spec => $association_end_spec,
allow_extra => 0,
);
croak "join_cols is present but empty"
if $assoc_end->{join_cols} && !@{$assoc_end->{join_cols}};
# transform multiplicity scalar into a pair [$min, $max]
$class->_parse_multiplicity($assoc_end);
$self->{$letter} = $assoc_end;
}
# set default association name
my @names = map {$self->{$_}{role} || $self->{$_}{table}{name}} qw/A B/;
$self->{name} ||= join "_", @names;
# if many-to-many, needs special treatment
my $install_method;
if ($self->{A}{multiplicity}[1] > 1 && $self->{B}{multiplicity}[1] > 1) {
$install_method = '_install_many_to_many';
}
# otherwise, treat as a regular association
else {
$install_method = '_install_path';
# handle implicit column names
if ($self->{A}{multiplicity}[1] > 1) { # n-to-1
$self->{B}{join_cols} ||= $self->{B}{table}{primary_key};
$self->{A}{join_cols} ||= $self->{B}{join_cols};
}
elsif ($self->{B}{multiplicity}[1] > 1) { # 1-to-n
$self->{A}{join_cols} ||= $self->{A}{table}{primary_key};
$self->{B}{join_cols} ||= $self->{A}{join_cols};
}
# check if we have the same number of columns on both sides
@{$self->{A}{join_cols}} == @{$self->{B}{join_cols}}
or croak "Association: numbers of columns do not match";
}
# instantiate
bless $self, $class;
# special checks for compositions
$self->_check_composition if $self->{kind} eq 'Composition';
# install methods from A to B and B to A, if role names are not empty
$self->{A}{role} || $self->{B}{role}
or croak "at least one side of the association must have a role name";
$self->$install_method(qw/A B/) if $self->{B}{role};
$self->$install_method(qw/B A/) if $self->{A}{role};
# EXPERIMENTAL : no longer need association ends; all info is stored in Paths
delete@{$self}{qw/A B/};
# avoid circular reference
weaken $self->{schema};
return $self;
}
# accessor methods
define_readonly_accessors(__PACKAGE__, qw/schema name kind path_AB path_BA/);
#----------------------------------------------------------------------
# PRIVATE UTILITY METHODS
#----------------------------------------------------------------------
sub _parse_multiplicity {
my ($class, $assoc_end) = @_;
# nothing to do if already an arrayref
return if ref $assoc_end->{multiplicity};
# otherwise, parse the scalar
$assoc_end->{multiplicity} =~ /^(?: # optional part
(\d+) # minimum
\s*\.\.\s* # followed by ".."
)? # end of optional part
(\d+|\*|n) # maximum
$/x
or croak "illegal multiplicity : $assoc_end->{multiplicity}";
# multiplicity '*' is a shortcut for '0..*', and
# multiplicity '1' is a shortcut for '1..1'.
my $max_is_star = !looks_like_number($2);
my $min = defined $1 ? $1 : ($max_is_star ? 0 : $2);
my $max = $max_is_star ? dualvar(POSIX::LONG_MAX, '*') : $2;
$assoc_end->{multiplicity} = [$min, $max];
}
sub _install_many_to_many {
my ($self, $from, $to) = @_;
# path must contain exactly 2 items (intermediate table + remote table)
my $role = $self->{$to}{role};
my @path = @{$self->{$to}{join_cols}};
@path == 2
or croak "many-to-many : should have exactly 2 roles";
# define the method
$self->{$from}{table}->define_navigation_method($role, @path);
}
sub _install_path {
my ($self, $from, $to) = @_;
# build the "ON" condition for SQL::Abstract::More
my $from_cols = $self->{$from}{join_cols};
my $to_cols = $self->{$to} {join_cols};
my %condition = pairwise {$a => $b} @$from_cols, @$to_cols;
# define path
( run in 2.262 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )