1 package DBIx::Class::ResultSource::MultipleTableInheritance;
5 use parent qw(DBIx::Class::ResultSource::View);
6 use Method::Signatures::Simple;
7 use Carp::Clan qw/^DBIx::Class/;
8 use aliased 'DBIx::Class::ResultSource::Table';
9 use aliased 'DBIx::Class::ResultClass::HashRefInflator';
10 use String::TT qw(strip tt);
11 use Scalar::Util qw(blessed);
12 use namespace::autoclean;
17 __PACKAGE__->mk_group_accessors(simple => qw(parent_source additional_parents));
21 # On construction, we hook $self->result_class->result_source_instance
22 # if present to get the superclass' source object
24 # When attached to a schema, we need to add sources to that schema with
25 # appropriate relationships for the foreign keys so the concrete tables
28 # We also generate our own view definition using this class' concrete table
29 # and the view for the superclass, and stored procedures for the insert,
30 # update and delete operations on this view.
32 # deploying the postgres rules through SQLT may be a pain though.
34 method new ($class: @args) {
35 my $new = $class->next::method(@args);
36 my $rc = $new->result_class;
37 if (my $meth = $rc->can('result_source_instance')) {
38 my $source = $rc->$meth;
39 if ($source->result_class ne $new->result_class
40 && $new->result_class->isa($source->result_class)) {
41 $new->parent_source($source);
47 method add_additional_parents (@classes) {
48 foreach my $class (@classes) {
49 Class::C3::Componentised->ensure_class_loaded($class);
50 $self->add_additional_parent(
51 $class->result_source_instance
56 method add_additional_parent ($source) {
57 my ($our_pk, $their_pk) = map {
58 join('|',sort $_->primary_columns)
61 confess "Can't attach additional parent ${\$source->name} - it has different PKs ($their_pk versus our $our_pk)"
62 unless $their_pk eq $our_pk;
63 $self->additional_parents([
64 @{$self->additional_parents||[]}, $source
68 $_ => # put the extra key first to default it
69 { originally_defined_in => $source->name, %{$source->column_info($_)}, },
70 } grep !$self->has_column($_), $source->columns
72 foreach my $rel ($source->relationships) {
73 my $rel_info = $source->relationship_info($rel);
74 $self->add_relationship(
75 $rel, $rel_info->{source}, $rel_info->{cond},
76 # extra key first to default it
77 {originally_defined_in => $source->name, %{$rel_info->{attrs}}},
81 push(@{$self->result_class.'::ISA'}, $source->result_class);
85 method _source_by_name ($name) {
86 my $schema = $self->schema;
88 grep { $_->name eq $name }
89 map $schema->source($_), $schema->sources;
90 confess "Couldn't find attached source for parent $name - did you use load_classes? This module is only compatible with load_namespaces"
95 method schema (@args) {
96 my $ret = $self->next::method(@args);
98 if ($self->parent_source) {
99 my $parent_name = $self->parent_source->name;
100 $self->parent_source($self->_source_by_name($parent_name));
102 $self->additional_parents([
103 map { $self->_source_by_name($_->name) }
104 @{$self->additional_parents||[]}
110 method attach_additional_sources () {
111 my $raw_name = $self->raw_source_name;
112 my $schema = $self->schema;
114 # if the raw source is already present we can assume we're done
115 return if grep { $_ eq $raw_name } $schema->sources;
117 # our parent should've been registered already actually due to DBIC
118 # attaching subclass sources later in load_namespaces
121 if ($self->parent_source) {
122 my $parent_name = $self->parent_source->name;
124 grep { $_->name eq $parent_name }
125 map $schema->source($_), $schema->sources;
126 confess "Couldn't find attached source for parent $parent_name - did you use load_classes? This module is only compatible with load_namespaces"
128 $self->parent_source($parent); # so our parent is the one in this schema
131 # create the raw table source
133 my $table = Table->new({ name => $self->raw_table_name });
135 # we don't need to add the PK cols explicitly if we're the root table
136 # since they'll get added below
141 foreach my $pri ($self->primary_columns) {
142 my %info = %{$self->column_info($pri)};
143 delete @info{qw(is_auto_increment sequence auto_nextval)};
144 $table->add_column($pri => \%info);
145 $pk_join{"foreign.${pri}"} = "self.${pri}";
147 # have to use source name lookups rather than result class here
148 # because we don't actually have a result class on the raw sources
149 $table->add_relationship('parent', $parent->raw_source_name, \%pk_join);
150 $self->deploy_depends_on->{$parent->result_class} = 1;
153 foreach my $add (@{$self->additional_parents||[]}) {
154 $table->add_relationship(
155 'parent_'.$add->name, $add->source_name, \%pk_join
157 $self->deploy_depends_on->{$add->result_class} = 1;
160 # add every column that's actually a concrete part of us
163 map { ($_ => { %{$self->column_info($_)} }) }
164 grep { $self->column_info($_)->{originally_defined_in} eq $self->name }
167 $table->set_primary_key($self->primary_columns);
169 # we need to copy our rels to the raw object as well
170 # note that ->add_relationship on a source object doesn't create an
171 # accessor so we can leave that part in the attributes
173 # if the other side is a table then we need to copy any rels it has
174 # back to us, as well, so that they point at the raw table. if the
175 # other side is an MTI view then we need to create the rels to it to
176 # point at -its- raw table; we don't need to worry about backrels because
177 # it's going to run this method too (and its raw source might not exist
178 # yet so we can't, anyway)
180 foreach my $rel ($self->relationships) {
181 my $rel_info = $self->relationship_info($rel);
183 # if we got this from the superclass, -its- raw table will nail this.
184 # if we got it from an additional parent, it's its problem.
185 next unless $rel_info->{attrs}{originally_defined_in} eq $self->name;
187 my $f_source = $schema->source($rel_info->{source});
189 # __PACKAGE__ is correct here because subclasses should be caught
191 my $one_of_us = $f_source->isa(__PACKAGE__);
193 my $f_source_name = $f_source->${\
194 ($one_of_us ? 'raw_source_name' : 'source_name')
197 $table->add_relationship(
198 '_'.$rel, $f_source_name, @{$rel_info}{qw(cond attrs)}
201 unless ($one_of_us) {
203 # we haven't been registered yet, so reverse_ cries
204 # XXX this is evil and will probably break eventually
205 local @{$schema->source_registrations}
206 {map $self->$_, qw(source_name result_class)}
208 $self->reverse_relationship_info($rel);
210 foreach my $rev_rel (keys %$reverse) {
211 $f_source->add_relationship(
212 '_raw_'.$rev_rel, $raw_name, @{$reverse->{$rev_rel}}{qw(cond attrs)}
218 $schema->register_source($raw_name => $table);
221 method set_primary_key (@args) {
222 if ($self->parent_source) {
223 confess "Can't set primary key on a subclass";
225 return $self->next::method(@args);
228 method raw_source_name () {
229 my $base = $self->source_name;
230 confess "Can't generate raw source name for ${\$self->name} when we don't have a source_name"
232 return 'Raw::'.$base;
235 method raw_table_name () {
236 return '_'.$self->name;
239 method add_columns (@args) {
240 my $ret = $self->next::method(@args);
241 $_->{originally_defined_in} ||= $self->name for values %{$self->_columns};
245 method add_relationship ($name, $f_source, $cond, $attrs) {
247 $name, $f_source, $cond,
248 { originally_defined_in => $self->name, %{$attrs||{}}, }
254 # helper routines, constructed as anon subs so autoclean nukes them
258 *argify = sub (@names) {
262 *qualify_with = sub ($source, @names) {
263 my $name = blessed($source) ? $source->name : $source;
264 map join('.', $name, $_), @names;
267 *body_cols = sub ($source) {
268 my %pk; @pk{$source->primary_columns} = ();
269 map +{ %{$source->column_info($_)}, name => $_ },
270 grep !exists $pk{$_}, $source->columns;
273 *pk_cols = sub ($source) {
274 map +{ %{$source->column_info($_)}, name => $_ },
275 $source->primary_columns;
278 *names_of = sub (@cols) { map $_->{name}, @cols };
280 *function_body = sub {
281 my ($name,$args,$body_parts) = @_;
284 map "_${\$_->{name}} ${\uc($_->{data_type})}",
287 my $body = join("\n", '', map " $_;", @$body_parts);
289 CREATE OR REPLACE FUNCTION [% name %]
291 RETURNS VOID AS $function$
295 $function$ LANGUAGE plpgsql;
298 #*function_body = sub ($name,$args,$body_parts) {
301 #map "_${\$_->{name}} ${\uc($_->{data_type})}",
304 #my $body = join("\n", '', map " $_;", @$body_parts);
306 #CREATE OR REPLACE FUNCTION [% name %]
308 #RETURNS VOID AS $function$
312 #$function$ LANGUAGE plpgsql;
321 *arg_hash = sub ($source) {
322 map +($_ => \(argify $_)), names_of body_cols $source;
325 *rule_body = sub ($on, $to, $oldlist, $newlist) {
326 my $arglist = join(', ',
327 (qualify_with 'OLD', names_of @$oldlist),
328 (qualify_with 'NEW', names_of @$newlist),
330 $to = $to->name if blessed($to);
332 CREATE RULE _[% to %]_[% on %]_rule AS
333 ON [% on | upper %] TO [% to %]
335 SELECT [% to %]_[% on %]([% arglist %])
341 method root_table () {
343 ? $self->parent_source->root_table
344 : $self->schema->source($self->raw_source_name)
347 method view_definition () {
348 my $schema = $self->schema;
349 confess "Can't generate view without connected schema, sorry"
350 unless $schema && $schema->storage;
351 my $sqla = $schema->storage->sql_maker;
352 my $table = $self->schema->source($self->raw_source_name);
353 my $super_view = $self->parent_source;
354 my @all_parents = my @other_parents = @{$self->additional_parents||[]};
355 push(@all_parents, $super_view) if defined($super_view);
356 my @sources = ($table, @all_parents);
357 my @body_cols = map body_cols($_), @sources;
358 my @pk_cols = pk_cols $self;
362 my $am_root = !($super_view || @other_parents);
364 my $select = $sqla->select(
367 : ([ # FROM _tbl _tbl
368 { $table->name => $table->name },
372 { $parent->name => $parent->name },
373 # ON _tbl.id = view.id
374 { map +(qualify_with($parent, $_), qualify_with($table, $_)),
380 [ (qualify_with $table, names_of @pk_cols), names_of @body_cols ],
383 my ($now, @next) = grep defined, $super_view, $table, @other_parents;
387 # NOTE: this assumes a single PK col called id with a sequence somewhere
388 # but nothing else -should- so fixing this should make everything work
391 $self->name.'_insert',
394 $sqla->insert( # INSERT INTO tbl/super_view (foo, ...) VALUES (_foo, ...)
399 $sqla->insert( # INSERT INTO parent (id, ...)
400 # VALUES (currval('_root_tbl_id_seq'), ...)
404 id => \"currval('${\$self->root_table->name}_id_seq')",
410 # note - similar to arg_hash but not quite enough to share code sanely
411 my $pk_where = { # id = _id AND id2 = _id2 ...
412 map +($_ => \"= ${\argify $_}"), names_of @pk_cols
419 $self->name.'_update',
420 [ @pk_cols, @body_cols ],
422 $_->name, # UPDATE foo
423 { arg_hash $_ }, # SET a = _a
432 $self->name.'_delete',
434 [ map $sqla->delete($_->name, $pk_where), @sources ];
437 (rule_body insert => $self, [], \@body_cols),
438 (rule_body update => $self, \@pk_cols, \@body_cols),
439 (rule_body delete => $self, \@pk_cols, []),
441 return join("\n\n", $select, $insert_func, $update_func, $delete_func, @rules);
450 DBIx::Class::ResultSource::MultipleTableInheritance
451 Use multiple tables to define your classes
455 This only works with PostgreSQL for the moment.
460 package Cafe::Result::Coffee;
464 use parent 'DBIx::Class::Core';
465 use aliased 'DBIx::Class::ResultSource::MultipleTableInheritance'
468 __PACKAGE__->table_class(MTI);
469 __PACKAGE__->table('coffee');
470 __PACKAGE__->add_columns(
471 "id", { data_type => "integer" },
474 default_value => "good" },
477 __PACKAGE__->set_primary_key("id");
483 package Cafe::Result::Sumatra;
485 use parent 'Cafe::Result::Coffee';
487 __PACKAGE__->table('sumatra');
489 __PACKAGE__->add_columns( "aroma",
490 { data_type => "text" }
498 my $schema = Cafe->connect($dsn,$user,$pass);
500 my $cup = $schema->resultset('Sumatra');
502 print STDERR Dwarn $cup->result_source->columns;
509 Inherit from this package and you can make a resultset class from a view, but
510 that's more than a little bit misleading: the result is B<transparently
513 This is accomplished through the use of stored procedures that map changes
514 written to the view to changes to the underlying concrete tables.
518 In many applications, many classes are subclasses of others. Let's say you
521 # Conceptual domain model
536 That's redundant. Hold on a sec...
544 class Investor extends User {
548 Good idea, but how to put this into code?
550 One far-too common and absolutely horrendous solution is to have a "checkbox"
551 in your database: a nullable "investor" column, which entails a nullable
552 "dollars" column, in the user table.
554 create table "user" (
555 "id" integer not null primary key autoincrement,
556 "name" text not null,
557 "password" text not null,
558 "investor" tinyint(1),
562 Let's not discuss that further.
564 A second, better, solution is to break out the two tables into user and
567 create table "user" (
568 "id" integer not null primary key autoincrement,
569 "name" text not null,
570 "password" text not null
573 create table "investor" (
574 "id" integer not null references user("id"),
578 So that investor's PK is just an FK to the user. We can clearly see the class
579 hierarchy here, in which investor is a subclass of user. In DBIx::Class
580 applications, this second strategy looks like:
582 my $user_rs = $schema->resultset('User');
583 my $new_user = $user_rs->create(
584 name => $args->{name},
585 password => $args->{password},
590 my $new_investor = $schema->resultset('Investor')->create(
592 dollars => $args->{dollars},
595 One can cope well with the second strategy, and it seems to be the most popular
600 There is a third strategy implemented here. Make the database do more of the
601 work: hide the nasty bits so we don't have to handle them unless we really want
602 to. It'll save us some typing and it'll make for more expressive code. What if
605 my $new_investor = $schema->resultset('Investor')->create(
606 name => $args->{name},
607 password => $args->{password},
608 dollars => $args->{dollars},
611 And have it Just Work? The user...
614 name => $args->{name},
615 password => $args->{password},
618 should be created behind the scenes, and the use of either user or investor
619 in your code should require no special handling. Deleting and updating
620 $new_investor should also delete or update the user row.
622 It does. User and investor are both views, their concrete tables abstracted
623 away behind a set of rules and triggers. You would expect the above DBIC
624 create statement to look like this in SQL:
626 INSERT INTO investor ("name","password","dollars") VALUES (...);
628 But using MTI, it is really this:
630 INSERT INTO _user_table ("username","password") VALUES (...);
631 INSERT INTO _investor_table ("id","dollars") VALUES (currval('_user_table_id_seq',...) );
633 For deletes, the triggers fire in reverse, to preserve referential integrity
634 (foreign key constraints). For instance:
636 my $investor = $schema->resultset('Investor')->find({id => $args->{id}});
641 DELETE FROM _investor_table WHERE ("id" = ?);
642 DELETE FROM _user_table WHERE ("id" = ?);
652 MTI find the parents, if any, of your resultset class and adds them to the
653 list of parent_sources for the table.
656 =item add_additional_parents
659 Continuing with coffee:
661 __PACKAGE__->result_source_instance->add_additional_parents(
663 MyApp::Schema::Result::Beverage
664 MyApp::Schema::Result::Liquid
668 This just lets you manually add additional parents beyond the ones MTI finds.
670 =item add_additional_parent
672 __PACKAGE__->result_source_instance->add_additional_parent(
673 MyApp::Schema::Result::Beverage
676 You can also add just one.
678 =item attach_additional_sources
680 MTI takes the parents' sources and relationships, creates a new
681 DBIx::Class::Table object from them, and registers this as a new, raw, source
686 print STDERR map { "$_\n" } MyApp::Schema->sources;
694 Raw::Sumatra will be used to generate the view.
696 =item view_definition
698 This takes the raw table and generates the view (and stored procedures) you will use.
704 Matt S. Trout, E<lt>mst@shadowcatsystems.co.ukE<gt>
708 Amiri Barksdale, E<lt>amiri@metalabel.comE<gt>
712 Copyright (c) 2010 the DBIx::Class::ResultSource::MultipleTableInheritance
713 L</AUTHOR> and L</CONTRIBUTORS> as listed above.
717 This library is free software; you can redistribute it and/or modify
718 it under the same terms as Perl itself.
723 L<DBIx::Class::ResultSource>