X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FSchema.pm;h=77bd865397e4dc4adc2fbf4a2698fdf86f265385;hb=c216324aa4b0f79ba056fbe74adbd735421e378a;hp=0b9b969f7755704e91fe4304e6279bf85baa7c57;hpb=75d079145a507a0e5ff89b2676d383f4fd1a5511;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/Schema.pm b/lib/DBIx/Class/Schema.pm index 0b9b969..77bd865 100644 --- a/lib/DBIx/Class/Schema.pm +++ b/lib/DBIx/Class/Schema.pm @@ -4,6 +4,8 @@ use strict; use warnings; use Carp::Clan qw/^DBIx::Class/; +use Scalar::Util qw/weaken/; +require Module::Find; use base qw/DBIx::Class/; @@ -11,6 +13,7 @@ __PACKAGE__->mk_classdata('class_mappings' => {}); __PACKAGE__->mk_classdata('source_registrations' => {}); __PACKAGE__->mk_classdata('storage_type' => '::DBI'); __PACKAGE__->mk_classdata('storage'); +__PACKAGE__->mk_classdata('exception_action'); =head1 NAME @@ -20,7 +23,7 @@ DBIx::Class::Schema - composable schemas package Library::Schema; use base qw/DBIx::Class::Schema/; - + # load Library::Schema::CD, Library::Schema::Book, Library::Schema::DVD __PACKAGE__->load_classes(qw/CD Book DVD/); @@ -36,7 +39,7 @@ DBIx::Class::Schema - composable schemas $password, { AutoCommit => 0 }, ); - + my $schema2 = Library::Schema->connect($coderef_returning_dbh); # fetch objects using Library::Schema::DVD @@ -63,7 +66,7 @@ particular which module inherits off which. =back -Registers a class which isa L. Equivalent to +Registers a class which isa DBIx::Class::ResultSourceProxy. Equivalent to calling: $schema->register_source($moniker, $component_class->result_source_instance); @@ -94,6 +97,7 @@ sub register_source { $reg{$moniker} = $source; $self->source_registrations(\%reg); $source->schema($self); + weaken($source->{schema}) if ref($self); if ($source->result_class) { my %map = %{$self->class_mappings}; $map{$source->result_class} = $moniker; @@ -167,6 +171,12 @@ For example: sub sources { return keys %{shift->source_registrations}; } +=head2 storage + + my $storage = $schema->storage; + +Returns the L object for this Schema. + =head2 resultset =over 4 @@ -219,15 +229,15 @@ Example: sub load_classes { my ($class, @params) = @_; - + my %comps_for; - + if (@params) { foreach my $param (@params) { if (ref $param eq 'ARRAY') { # filter out commented entries my @modules = grep { $_ !~ /^#/ } @$param; - + push (@{$comps_for{$class}}, @modules); } elsif (ref $param eq 'HASH') { @@ -245,10 +255,6 @@ sub load_classes { } } } else { - eval "require Module::Find;"; - $class->throw_exception( - "No arguments to load_classes and couldn't load Module::Find ($@)" - ) if $@; my @comp = map { substr $_, length "${class}::" } Module::Find::findallmod($class); $comps_for{$class} = \@comp; @@ -261,13 +267,17 @@ sub load_classes { foreach my $prefix (keys %comps_for) { foreach my $comp (@{$comps_for{$prefix}||[]}) { my $comp_class = "${prefix}::${comp}"; - eval "use $comp_class"; # If it fails, assume the user fixed it - if ($@) { - $comp_class =~ s/::/\//g; - die $@ unless $@ =~ /Can't locate.+$comp_class\.pm\sin\s\@INC/; - warn $@ if $@; + { # try to untaint module name. mods where this fails + # are left alone so we don't have to change the old behavior + no locale; # localized \w doesn't untaint expression + if ( $comp_class =~ m/^( (?:\w+::)* \w+ )$/x ) { + $comp_class = $1; + } } - push(@to_register, [ $comp, $comp_class ]); + $class->ensure_class_loaded($comp_class); + $comp_class->source_name($comp) unless $comp_class->source_name; + + push(@to_register, [ $comp_class->source_name, $comp_class ]); } } } @@ -279,7 +289,165 @@ sub load_classes { } } -=head2 compose_connection +=head2 load_namespaces + +=over 4 + +=item Arguments: %options? + +=back + +This is an alternative to L above which assumes an alternative +layout for automatic class loading. It assumes that all result +classes are underneath a sub-namespace of the schema called C, any +corresponding ResultSet classes are underneath a sub-namespace of the schema +called C. + +Both of the sub-namespaces are configurable if you don't like the defaults, +via the options C and C. + +If (and only if) you specify the option C, any found +Result classes for which we do not find a corresponding +ResultSet class will have their C set to +C. + +C takes care of calling C for you where +neccessary if you didn't do it for yourself. + +All of the namespace and classname options to this method are relative to +the schema classname by default. To specify a fully-qualified name, prefix +it with a literal C<+>. + +Examples: + + # load My::Schema::Result::CD, My::Schema::Result::Artist, + # My::Schema::ResultSet::CD, etc... + My::Schema->load_namespaces; + + # Override everything to use ugly names. + # In this example, if there is a My::Schema::Res::Foo, but no matching + # My::Schema::RSets::Foo, then Foo will have its + # resultset_class set to My::Schema::RSetBase + My::Schema->load_namespaces( + result_namespace => 'Res', + resultset_namespace => 'RSets', + default_resultset_class => 'RSetBase', + ); + + # Put things in other namespaces + My::Schema->load_namespaces( + result_namespace => '+Some::Place::Results', + resultset_namespace => '+Another::Place::RSets', + ); + +If you'd like to use multiple namespaces of each type, simply use an arrayref +of namespaces for that option. In the case that the same result +(or resultset) class exists in multiple namespaces, the latter entries in +your list of namespaces will override earlier ones. + + My::Schema->load_namespaces( + # My::Schema::Results_C::Foo takes precedence over My::Schema::Results_B::Foo : + result_namespace => [ 'Results_A', 'Results_B', 'Results_C' ], + resultset_namespace => [ '+Some::Place::RSets', 'RSets' ], + ); + +=cut + +# Pre-pends our classname to the given relative classname or +# class namespace, unless there is a '+' prefix, which will +# be stripped. +sub _expand_relative_name { + my ($class, $name) = @_; + return if !$name; + $name = $class . '::' . $name if ! ($name =~ s/^\+//); + return $name; +} + +# returns a hash of $shortname => $fullname for every package +# found in the given namespaces ($shortname is with the $fullname's +# namespace stripped off) +sub _map_namespaces { + my ($class, @namespaces) = @_; + + my @results_hash; + foreach my $namespace (@namespaces) { + push( + @results_hash, + map { (substr($_, length "${namespace}::"), $_) } + Module::Find::findallmod($namespace) + ); + } + + @results_hash; +} + +sub load_namespaces { + my ($class, %args) = @_; + + my $result_namespace = delete $args{result_namespace} || 'Result'; + my $resultset_namespace = delete $args{resultset_namespace} || 'ResultSet'; + my $default_resultset_class = delete $args{default_resultset_class}; + + $class->throw_exception('load_namespaces: unknown option(s): ' + . join(q{,}, map { qq{'$_'} } keys %args)) + if scalar keys %args; + + $default_resultset_class + = $class->_expand_relative_name($default_resultset_class); + + for my $arg ($result_namespace, $resultset_namespace) { + $arg = [ $arg ] if !ref($arg) && $arg; + + $class->throw_exception('load_namespaces: namespace arguments must be ' + . 'a simple string or an arrayref') + if ref($arg) ne 'ARRAY'; + + $_ = $class->_expand_relative_name($_) for (@$arg); + } + + my %results = $class->_map_namespaces(@$result_namespace); + my %resultsets = $class->_map_namespaces(@$resultset_namespace); + + my @to_register; + { + no warnings 'redefine'; + local *Class::C3::reinitialize = sub { }; + use warnings 'redefine'; + + foreach my $result (keys %results) { + my $result_class = $results{$result}; + $class->ensure_class_loaded($result_class); + $result_class->source_name($result) unless $result_class->source_name; + + my $rs_class = delete $resultsets{$result}; + my $rs_set = $result_class->resultset_class; + if($rs_set && $rs_set ne 'DBIx::Class::ResultSet') { + if($rs_class && $rs_class ne $rs_set) { + warn "We found ResultSet class '$rs_class' for '$result', but it seems " + . "that you had already set '$result' to use '$rs_set' instead"; + } + } + elsif($rs_class ||= $default_resultset_class) { + $class->ensure_class_loaded($rs_class); + $result_class->resultset_class($rs_class); + } + + push(@to_register, [ $result_class->source_name, $result_class ]); + } + } + + foreach (sort keys %resultsets) { + warn "load_namespaces found ResultSet class $_ with no " + . 'corresponding Result class'; + } + + Class::C3->reinitialize; + $class->register_class(@$_) for (@to_register); + + return; +} + +=head2 compose_connection (DEPRECATED) =over 4 @@ -289,6 +457,12 @@ sub load_classes { =back +DEPRECATED. You probably wanted compose_namespace. + +Actually, you probably just wanted to call connect. + +=for hidden due to deprecation + Calls L to the target namespace, calls L with @db_info on the new schema, then injects the L component and a @@ -303,43 +477,50 @@ more information. =cut -sub compose_connection { - my ($self, $target, @info) = @_; - my $base = 'DBIx::Class::ResultSetProxy'; - eval "require ${base};"; - $self->throw_exception - ("No arguments to load_classes and couldn't load ${base} ($@)") - if $@; - - if ($self eq $target) { - # Pathological case, largely caused by the docs on early C::M::DBIC::Plain - foreach my $moniker ($self->sources) { - my $source = $self->source($moniker); +{ + my $warn; + + sub compose_connection { + my ($self, $target, @info) = @_; + + warn "compose_connection deprecated as of 0.08000" unless $warn++; + + my $base = 'DBIx::Class::ResultSetProxy'; + eval "require ${base};"; + $self->throw_exception + ("No arguments to load_classes and couldn't load ${base} ($@)") + if $@; + + if ($self eq $target) { + # Pathological case, largely caused by the docs on early C::M::DBIC::Plain + foreach my $moniker ($self->sources) { + my $source = $self->source($moniker); + my $class = $source->result_class; + $self->inject_base($class, $base); + $class->mk_classdata(resultset_instance => $source->resultset); + $class->mk_classdata(class_resolver => $self); + } + $self->connection(@info); + return $self; + } + + my $schema = $self->compose_namespace($target, $base); + { + no strict 'refs'; + *{"${target}::schema"} = sub { $schema }; + } + + $schema->connection(@info); + foreach my $moniker ($schema->sources) { + my $source = $schema->source($moniker); my $class = $source->result_class; - $self->inject_base($class, $base); + #warn "$moniker $class $source ".$source->storage; + $class->mk_classdata(result_source_instance => $source); $class->mk_classdata(resultset_instance => $source->resultset); - $class->mk_classdata(class_resolver => $self); + $class->mk_classdata(class_resolver => $schema); } - $self->connection(@info); - return $self; - } - - my $schema = $self->compose_namespace($target, $base); - { - no strict 'refs'; - *{"${target}::schema"} = sub { $schema }; - } - - $schema->connection(@info); - foreach my $moniker ($schema->sources) { - my $source = $schema->source($moniker); - my $class = $source->result_class; - #warn "$moniker $class $source ".$source->storage; - $class->mk_classdata(result_source_instance => $source); - $class->mk_classdata(resultset_instance => $source->resultset); - $class->mk_classdata(class_resolver => $schema); + return $schema; } - return $schema; } =head2 compose_namespace @@ -391,6 +572,8 @@ sub compose_namespace { $target_class => $source->result_class, ($base ? $base : ()) ); $source->result_class($target_class); + $target_class->result_source_instance($source) + if $target_class->can('result_source_instance'); } } Class::C3->reinitialize(); @@ -424,6 +607,26 @@ sub setup_connection_class { $target->connection(@info); } +=head2 storage_type + +=over 4 + +=item Arguments: $storage_type + +=item Return Value: $storage_type + +=back + +Set the storage class that will be instantiated when L is called. +If the classname starts with C<::>, the prefix C is +assumed by L. Defaults to C<::DBI>, +which is L. + +You want to use this to hardcoded subclasses of L +in cases where the appropriate subclass is not autodetected, such as when +dealing with MSSQL via L, in which case you'd set it to +C<::DBI::Sybase::MSSQL>. + =head2 connection =over 4 @@ -436,8 +639,10 @@ sub setup_connection_class { Instantiates a new Storage object of type L and passes the arguments to -$storage->connect_info. Sets the connection in-place on the schema. See -L for more information. +$storage->connect_info. Sets the connection in-place on the schema. + +See L for DBI-specific syntax, +or L in general. =cut @@ -451,7 +656,7 @@ sub connection { $self->throw_exception( "No arguments to load_classes and couldn't load ${storage_class} ($@)" ) if $@; - my $storage = $storage_class->new; + my $storage = $storage_class->new($self); $storage->connect_info(\@info); $self->storage($storage); return $self; @@ -475,141 +680,83 @@ information. sub connect { shift->clone->connection(@_) } -=head2 txn_begin - -Begins a transaction (does nothing if AutoCommit is off). Equivalent to -calling $schema->storage->txn_begin. See -L for more information. - -=cut - -sub txn_begin { shift->storage->txn_begin } +=head2 txn_do -=head2 txn_commit +=over 4 -Commits the current transaction. Equivalent to calling -$schema->storage->txn_commit. See L -for more information. +=item Arguments: C<$coderef>, @coderef_args? -=cut +=item Return Value: The return value of $coderef -sub txn_commit { shift->storage->txn_commit } +=back -=head2 txn_rollback +Executes C<$coderef> with (optional) arguments C<@coderef_args> atomically, +returning its result (if any). Equivalent to calling $schema->storage->txn_do. +See L for more information. -Rolls back the current transaction. Equivalent to calling -$schema->storage->txn_rollback. See -L for more information. +This interface is preferred over using the individual methods L, +L, and L below. =cut -sub txn_rollback { shift->storage->txn_rollback } - -=head2 txn_do +sub txn_do { + my $self = shift; -=over 4 + $self->storage or $self->throw_exception + ('txn_do called on $schema without storage'); -=item Arguments: C<$coderef>, @coderef_args? + $self->storage->txn_do(@_); +} -=item Return Value: The return value of $coderef +=head2 txn_begin -=back +Begins a transaction (does nothing if AutoCommit is off). Equivalent to +calling $schema->storage->txn_begin. See +L for more information. -Executes C<$coderef> with (optional) arguments C<@coderef_args> atomically, -returning its result (if any). If an exception is caught, a rollback is issued -and the exception is rethrown. If the rollback fails, (i.e. throws an -exception) an exception is thrown that includes a "Rollback failed" message. +=cut -For example, +sub txn_begin { + my $self = shift; - my $author_rs = $schema->resultset('Author')->find(1); + $self->storage or $self->throw_exception + ('txn_begin called on $schema without storage'); - my $coderef = sub { - my ($author, @titles) = @_; + $self->storage->txn_begin; +} - # If any one of these fails, the entire transaction fails - $author->create_related('books', { - title => $_ - }) foreach (@titles); +=head2 txn_commit - return $author->books; - }; +Commits the current transaction. Equivalent to calling +$schema->storage->txn_commit. See L +for more information. - my $rs; - eval { - $rs = $schema->txn_do($coderef, $author_rs, qw/Night Day It/); - }; +=cut - if ($@) { - my $error = $@; - if ($error =~ /Rollback failed/) { - die "something terrible has happened!"; - } else { - deal_with_failed_transaction(); - } - } +sub txn_commit { + my $self = shift; -In a nested transaction (calling txn_do() from within a txn_do() coderef) only -the outermost transaction will issue a L on -the Schema's storage, and txn_do() can be called in void, scalar and list -context and it will behave as expected. + $self->storage or $self->throw_exception + ('txn_commit called on $schema without storage'); -=cut + $self->storage->txn_commit; +} -sub txn_do { - my ($self, $coderef, @args) = @_; - - ref $self or $self->throw_exception - ('Cannot execute txn_do as a class method'); - ref $coderef eq 'CODE' or $self->throw_exception - ('$coderef must be a CODE reference'); - - my (@return_values, $return_value); - - $self->txn_begin; # If this throws an exception, no rollback is needed - - my $wantarray = wantarray; # Need to save this since the context - # inside the eval{} block is independent - # of the context that called txn_do() - eval { - - # Need to differentiate between scalar/list context to allow for - # returning a list in scalar context to get the size of the list - if ($wantarray) { - # list context - @return_values = $coderef->(@args); - } elsif (defined $wantarray) { - # scalar context - $return_value = $coderef->(@args); - } else { - # void context - $coderef->(@args); - } - $self->txn_commit; - }; +=head2 txn_rollback - if ($@) { - my $error = $@; +Rolls back the current transaction. Equivalent to calling +$schema->storage->txn_rollback. See +L for more information. - eval { - $self->txn_rollback; - }; +=cut - if ($@) { - my $rollback_error = $@; - my $exception_class = "DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION"; - $self->throw_exception($error) # propagate nested rollback - if $rollback_error =~ /$exception_class/; +sub txn_rollback { + my $self = shift; - $self->throw_exception( - "Transaction aborted: $error. Rollback failed: ${rollback_error}" - ); - } else { - $self->throw_exception($error); # txn failed but rollback succeeded - } - } + $self->storage or $self->throw_exception + ('txn_rollback called on $schema without storage'); - return $wantarray ? @return_values : $return_value; + $self->storage->txn_rollback; } =head2 clone @@ -627,12 +774,15 @@ copy. sub clone { my ($self) = @_; - my $clone = bless({ (ref $self ? %$self : ()) }, ref $self || $self); + my $clone = { (ref $self ? %$self : ()) }; + bless $clone, (ref $self || $self); + foreach my $moniker ($self->sources) { my $source = $self->source($moniker); my $new = $source->new($source); $clone->register_source($moniker => $new); } + $clone->storage->set_schema($clone) if $clone->storage; return $clone; } @@ -640,13 +790,22 @@ sub clone { =over 4 -=item Arguments: $moniker, \@data; +=item Arguments: $source_name, \@data; =back -Populates the source registered with the given moniker with the supplied data. -@data should be a list of listrefs -- the first containing column names, the -second matching values. +Pass this method a resultsource name, and an arrayref of +arrayrefs. The arrayrefs should contain a list of column names, +followed by one or many sets of matching data for the given columns. + +In void context, C in L is used +to insert the data, as this is a fast method. However, insert_bulk currently +assumes that your datasets all contain the same type of values, using scalar +references in a column in one row, and not in another will probably not work. + +Otherwise, each set of data is inserted into the database using +L, and a arrayref of the resulting row +objects is returned. i.e., @@ -663,15 +822,50 @@ sub populate { my ($self, $name, $data) = @_; my $rs = $self->resultset($name); my @names = @{shift(@$data)}; - my @created; - foreach my $item (@$data) { - my %create; - @create{@names} = @$item; - push(@created, $rs->create(\%create)); + if(defined wantarray) { + my @created; + foreach my $item (@$data) { + my %create; + @create{@names} = @$item; + push(@created, $rs->create(\%create)); + } + return @created; } - return @created; + $self->storage->insert_bulk($self->source($name)->from, \@names, $data); } +=head2 exception_action + +=over 4 + +=item Arguments: $code_reference + +=back + +If C is set for this class/object, L +will prefer to call this code reference with the exception as an argument, +rather than its normal action. + +Your subroutine should probably just wrap the error in the exception +object/class of your choosing and rethrow. If, against all sage advice, +you'd like your C to suppress a particular exception +completely, simply have it return true. + +Example: + + package My::Schema; + use base qw/DBIx::Class::Schema/; + use My::ExceptionClass; + __PACKAGE__->exception_action(sub { My::ExceptionClass->throw(@_) }); + __PACKAGE__->load_classes; + + # or: + my $schema_obj = My::Schema->connect( .... ); + $schema_obj->exception_action(sub { My::ExceptionClass->throw(@_) }); + + # suppress all exceptions, like a moron: + $schema_obj->exception_action(sub { 1 }); + =head2 throw_exception =over 4 @@ -681,20 +875,21 @@ sub populate { =back Throws an exception. Defaults to using L to report errors from -user's perspective. +user's perspective. See L for details on overriding +this method's behavior. =cut sub throw_exception { - my ($self) = shift; - croak @_; + my $self = shift; + croak @_ if !$self->exception_action || !$self->exception_action->(@_); } =head2 deploy (EXPERIMENTAL) =over 4 -=item Arguments: $sqlt_args +=item Arguments: $sqlt_args, $dir =back @@ -703,12 +898,58 @@ Attempts to deploy the schema to the current storage using L. Note that this feature is currently EXPERIMENTAL and may not work correctly across all databases, or fully handle complex relationships. +See L for a list of values for C<$sqlt_args>. The most +common value for this would be C<< { add_drop_table => 1, } >> to have the SQL +produced include a DROP TABLE statement for each table created. + =cut sub deploy { - my ($self, $sqltargs) = @_; + my ($self, $sqltargs, $dir) = @_; $self->throw_exception("Can't deploy without storage") unless $self->storage; - $self->storage->deploy($self, undef, $sqltargs); + $self->storage->deploy($self, undef, $sqltargs, $dir); +} + +=head2 create_ddl_dir (EXPERIMENTAL) + +=over 4 + +=item Arguments: \@databases, $version, $directory, $sqlt_args + +=back + +Creates an SQL file based on the Schema, for each of the specified +database types, in the given directory. + +Note that this feature is currently EXPERIMENTAL and may not work correctly +across all databases, or fully handle complex relationships. + +=cut + +sub create_ddl_dir { + my $self = shift; + + $self->throw_exception("Can't create_ddl_dir without storage") unless $self->storage; + $self->storage->create_ddl_dir($self, @_); +} + +=head2 ddl_filename (EXPERIMENTAL) + + my $filename = $table->ddl_filename($type, $dir, $version) + +Creates a filename for a SQL file based on the table class name. Not +intended for direct end user use. + +=cut + +sub ddl_filename { + my ($self, $type, $dir, $version) = @_; + + my $filename = ref($self); + $filename =~ s/::/-/; + $filename = "$dir$filename-$version-$type.sql"; + + return $filename; } 1; @@ -722,4 +963,3 @@ Matt S. Trout You may distribute this code under the same terms as Perl itself. =cut -