view Lib/Schema/DataSource/CDBIBuilder.pm @ 42:4ff27cd051e3

updated ORM schema model
author Sergey
date Thu, 07 Jan 2010 15:34:42 +0300
parents 03e58a454b20
children 16ada169ca75
line wrap: on
line source

use strict;
package Schema::DataSource::CDBIBuilder;
use Schema::DataSource::TypeMapping;
use Common;
our @ISA = qw(Object);

BEGIN {
    DeclareProperty ClassMappings => ACCESS_NONE;
    DeclareProperty TypeMapping => ACCESS_READ;
    DeclareProperty ValueTypeReflections => ACCESS_READ;
}

sub CTOR {
    my ($this,%args) = @_;
    
    $this->{$TypeMapping} = $args{'TypeMapping'} || Schema::DataSource::TypeMapping::Std->new;
    $this->{$ValueTypeReflections} = { DateTime => 'DateTime'};
}

sub ReflectValueType {
    my ($this,$Type) = @_;
    return $this->{$ValueTypeReflections}{$Type->Name->Simple};
}

sub GetClassMapping {
    my ($this,$type) = @_;
    
    if (my $mapping = $this->{$ClassMappings}->{$type->Name->Canonical}) {
        return $mapping;
    } else {
        $mapping = new Schema::DataSource::CDBIBuilder::ClassMapping(Class => $type,Parent => $this);
        $this->{$ClassMappings}{$type->Name->Canonical} = $mapping;
        return $mapping
    }
}

sub EnumClassMappings {
    my ($this) = @_;
    return $this->{$ClassMappings} ? values %{$this->{$ClassMappings}} : ();
}

sub AddType {
    my ($this,$type) = @_;
    $this->GetClassMapping($type);
}

sub BuildDBSchema {
    my ($this) = @_;
    
    my $schemaDB = new Schema::DB(Name => 'auto', Version => time);
    
    if ($this->{$ClassMappings}) {
        $_->CreateTable($schemaDB) foreach values %{ $this->{$ClassMappings} };
        $_->CreateConstraints($schemaDB) foreach values %{ $this->{$ClassMappings} };
    }
    
    return $schemaDB;
}

sub WriteModules {
    my ($this,$fileName,$prefix) = @_;
    
    my $text;
    $text = <<ModuleHeader;
#autogenerated script don't edit
package ${prefix}DBI;
use base 'Class::DBI';

require DateTime;

our (\$DSN,\$User,\$Password,\$Init);
\$DSN ||= 'DBI:null'; # avoid warning

__PACKAGE__->connection(\$DSN,\$User,\$Password);

# initialize
foreach my \$action (ref \$Init eq 'ARRAY' ? \@{\$Init} : \$Init) {
    next unless \$action;
    
    if (ref \$action eq 'CODE') {
        \$action->(__PACKAGE__->db_Main);
    } elsif (not ref \$action) {
        __PACKAGE__->db_Main->do(\$action);
    }
}

ModuleHeader
    
    if ($this->{$ClassMappings}) {
        $text .= join ("\n\n", map $_->GenerateText($prefix.'DBI',$prefix), sort {$a->Class->Name->Canonical cmp $b->Class->Name->Canonical } values %{ $this->{$ClassMappings} } );
    }
    
    $text .= "\n1;";
    
    open my $out, ">$fileName" or die new Exception("Failed to open file",$fileName,$!);
    print $out $text;
}

sub Dispose {
    my ($this) = @_;
    
    delete @$this{$ClassMappings,$TypeMapping,$ValueTypeReflections};
    
    $this->SUPER::Dispose;
}

package Schema::DataSource::CDBIBuilder::ClassMapping;
use Common;
use Schema;
our @ISA = qw(Object);

BEGIN {
    DeclareProperty Table => ACCESS_READ;
    DeclareProperty PropertyTables => ACCESS_READ;
    DeclareProperty PropertyMappings => ACCESS_READ;
    
    DeclareProperty Class => ACCESS_READ;
    DeclareProperty Parent => ACCESS_NONE;
}

sub CTOR {
    my ($this,%args) = @_;
    
    $this->{$Class} = $args{'Class'} or die new Exception('The class must be specified');
    $this->{$Parent} = $args{'Parent'} or die new Exception('The parent must be specified');
    
}

sub PropertyMapping {
    my ($this,%args) = @_;
    $this->{$PropertyMappings}{$args{'name'}} = { Column => $args{'Column'},DBType => $args{'DBType'} };
}

sub CreateTable {
    my ($this,$schemaDB) = @_;
    
    return if $this->{$Class}->isTemplate or $this->{$Class}->GetAttribute('ValueType') or $this->{$Class}->Name->Simple eq 'Set';
    
    # CreateTable
    my $table = $schemaDB->AddTable({Name => $this->{$Class}->Name->Canonical});
    $table->InsertColumn({
        Name => '_id',
        Type => $this->{$Parent}->TypeMapping->DBIdentifierType,
        Tag => ['AUTO_INCREMENT']
    });
    $table->SetPrimaryKey('_id');
    foreach my $prop ( grep { UNIVERSAL::isa($_,'Schema::Property') } $this->{$Class}->ListMembers ) {
        if ($prop->Type->Name->Name eq 'Set') {
            # special case for multiple values
            my $propTable = $this->CreatePropertyTable($schemaDB,$prop);
            $propTable->LinkTo($table,'parent');
        } else {
            $table->InsertColumn({
                Name => $prop->Name,
                Type => $this->{$Parent}->TypeMapping->MapType($prop->Type),
                CanBeNull => 1
            });
        }
    }
    $this->{$Table} = $table;
    return $table;
}

sub CreatePropertyTable {
    my ($this,$schemaDB,$property) = @_;
    
    my $table = $schemaDB->AddTable({Name => $this->{$Class}->Name->Canonical.'_'.$property->Name});
    $table->InsertColumn({
        Name => '_id',
        Type => $this->{$Parent}->TypeMapping->DBIdentifierType,
        Tag => ['AUTO_INCREMENT']
    });
    $table->SetPrimaryKey('_id');
    
    $table->InsertColumn({
        Name => 'parent',
        Type => $this->{$Parent}->TypeMapping->DBIdentifierType
    });
    
    $table->InsertColumn({
        Name => 'value',
        Type => $this->{$Parent}->TypeMapping->MapType($property->Type->GetAttribute('TemplateInstance')->{'Parameters'}{'T'}),
        CanBeNull => 1
    });
    
    $this->{$PropertyTables}->{$property->Name} = $table;
    
    return $table;
}

sub CreateConstraints {
    my ($this,$schemaDB) = @_;
    return if $this->{$Class}->isTemplate or $this->{$Class}->GetAttribute('ValueType') or $this->{$Class}->Name->Simple eq 'Set';
    
    foreach my $prop ( grep { UNIVERSAL::isa($_,'Schema::Property') } $this->{$Class}->ListMembers ) {
        if ($prop->Type->Name->Name eq 'Set' ) {
            # special case for multiple values
            if (not $prop->Type->GetAttribute('TemplateInstance')->{'Parameters'}{'T'}->GetAttribute('ValueType')) {
                $this->{$PropertyTables}->{$prop->Name}->LinkTo(
                    $this->{$Parent}->GetClassMapping($prop->Type->GetAttribute('TemplateInstance')->{'Parameters'}{'T'})->Table,
                    'value'
                );
            }
        } elsif (not $prop->Type->GetAttribute('ValueType')) {
            $this->{$Table}->LinkTo(
                scalar($this->{$Parent}->GetClassMapping($prop->Type)->Table),
                $prop->Name
            );
        }
    }
}

sub GeneratePropertyTableText {
    my ($this,$prop,$baseModule,$prefix) = @_;
    
    my $packageName = $this->GeneratePropertyClassName($prop,$prefix);
    my $tableName = $this->{$PropertyTables}->{$prop->Name}->Name;
    my $parentName = $this->GenerateClassName($prefix);
    my $text .= "package $packageName;\n";
    $text .= "use base '$baseModule';\n\n";
    $text .= "__PACKAGE__->table('`$tableName`');\n";
    $text .= "__PACKAGE__->columns(Essential => qw/_id parent value/);\n";
    $text .= "__PACKAGE__->has_a( parent => '$parentName');\n";
    
    my $typeValue;
    if ($prop->Type->Name->Simple eq 'Set') {
        $typeValue = $prop->Type->GetAttribute('TemplateInstance')->{'Parameters'}{'T'};
    } else {
        $typeValue = $prop->Type;
    }
    if ($typeValue->GetAttribute('ValueType')) {
        if (my $reflectedClass = $this->{$Parent}->ReflectValueType($typeValue)) {
            $text .= "__PACKAGE__->has_a( value => '$reflectedClass');\n";
        }
    } else {
        my $foreignName = $this->{$Parent}->GetClassMapping($prop->Type->GetAttribute('TemplateInstance')->{'Parameters'}{'T'})->GenerateClassName($prefix);
        $text .= "__PACKAGE__->has_a( value => '$foreignName');\n";
    }
    
    return $text;
}

sub GeneratePropertyClassName {
    my ($this,$prop,$prefix) = @_;
    
    my $packageName = $this->{$Class}->Name->Canonical;
    $packageName =~ s/\W//g;
    return $prefix.$packageName.$prop->Name.'Ref';
}

sub GenerateClassName {
    my ($this,$prefix) = @_;
    my $packageName = $this->{$Class}->Name->Canonical;
    $packageName =~ s/\W//g;
    return $prefix. $packageName;
}

sub GenerateText {
    my ($this,$baseModule,$prefix) = @_;
    
    return if $this->{$Class}->isTemplate or $this->{$Class}->GetAttribute('ValueType') or $this->{$Class}->Name->Simple eq 'Set';
    
    my @PropertyModules;
    my $text;
    my $packageName = $this->GenerateClassName($prefix);
    
    my $tableName = $this->{$Table}->Name;
    my $listColumns = join ',', map { '\''. $_->Name . '\''} $this->{$Table}->Columns;
    
    $text .= "package $packageName;\n";
    $text .= "use base '$baseModule'". ($this->{$Class}->Name->Name eq 'Map' ? ',\'CDBI::Map\'' : '' ).";\n\n";
    
    $text .= "__PACKAGE__->table('`$tableName`');\n";
    $text .= "__PACKAGE__->columns(Essential => $listColumns);\n";
    
    foreach my $prop ( grep { UNIVERSAL::isa($_,'Schema::Property') } $this->{$Class}->ListMembers ) {
        my $propName = $prop->Name;
        if ($prop->Type->Name->Name eq 'Set') {
            # has_many
            push @PropertyModules, $this->GeneratePropertyTableText($prop,$baseModule,$prefix);
            my $propClass = $this->GeneratePropertyClassName($prop,$prefix);
            $text .= <<ACCESSORS;
__PACKAGE__->has_many( ${propName}_ref => '$propClass');
sub $propName {
    return map { \$_->value } ${propName}_ref(\@_);
}
sub add_to_$propName {
    return add_to_${propName}_ref(\@_);
}
ACCESSORS
            
        } elsif (not $prop->Type->GetAttribute('ValueType')) {
            # has_a
            my $ForeignClass = $this->{$Parent}->GetClassMapping($prop->Type)->GenerateClassName($prefix);
            $text .= "__PACKAGE__->has_a( $propName => '$ForeignClass');\n";
        } else {
            if (my $reflectedClass = $this->{$Parent}->ReflectValueType($prop->Type)) {
                $text .= "__PACKAGE__->has_a( $propName => '$reflectedClass');\n";
            }
        }
    }
    
    # создаем список дочерних классов
    foreach my $descedantMapping (grep {$_->{$Class}->isType($this->{$Class},1)} $this->{$Parent}->EnumClassMappings) {
        next if $descedantMapping == $this;
        $text .= "__PACKAGE__->might_have('m".$descedantMapping->GenerateClassName('')."' => '".$descedantMapping->GenerateClassName($prefix)."');\n";
    }
    
    # создаем ссылки на все классы, которые могут ссылаться на наш
    # вид свойства ссылки: refererClassProp
    foreach my $referer (grep {not $_->Class->isTemplate} $this->{$Parent}->EnumClassMappings) {
        next if $referer == $this;
        foreach my $prop ( grep { $_->isa('Schema::Property') } $referer->{$Class}->ListMembers ) {
            if($prop->Type->Equals($this->{$Class})) {
                $text .= "__PACKAGE__->has_many('referer".$referer->GenerateClassName('').$prop->Name."' => '".$referer->GenerateClassName($prefix)."','".$prop->Name."');\n";
            } elsif ($prop->Type->Name->Name eq 'Set' and $this->{$Class}->Equals($prop->Type->GetAttribute('TemplateInstance')->{'Parameters'}{'T'}) ) {
                # если класс был параметром множества и $prop->Type и есть это множество
                $text .= "__PACKAGE__->has_many('referer".$referer->GeneratePropertyClassName($prop,'')."value' => '".$referer->GeneratePropertyClassName($prop,$prefix)."','value');\n";
            }
        }
    }
    
    return (@PropertyModules,$text);
}

1;