Beginning of docs. Making headway. Next is subroutine docs
[dbsrgits/DBIx-Class-ResultSource-MultipleTableInheritance.git] / lib / DBIx / Class / ResultSource / MultipleTableInheritance.pm
CommitLineData
876f6525 1package DBIx::Class::ResultSource::MultipleTableInheritance;
2
3use strict;
4use warnings;
5use parent qw(DBIx::Class::ResultSource::View);
876f6525 6use Method::Signatures::Simple;
7use Carp::Clan qw/^DBIx::Class/;
ca79850d 8use aliased 'DBIx::Class::ResultSource::Table';
7abe3af2 9use aliased 'DBIx::Class::ResultClass::HashRefInflator';
05fd2477 10use String::TT qw(strip tt);
92ebfc06 11use Scalar::Util qw(blessed);
ca79850d 12use namespace::autoclean;
70d56286 13
146ec120 14our $VERSION = 0.01;
70d56286 15
803ffff2 16__PACKAGE__->mk_group_accessors(simple => qw(parent_source additional_parents));
876f6525 17
18method new ($class: @args) {
19 my $new = $class->next::method(@args);
20 my $rc = $new->result_class;
21 if (my $meth = $rc->can('result_source_instance')) {
7abe3af2 22 my $source = $rc->$meth;
23 if ($source->result_class ne $new->result_class
24 && $new->result_class->isa($source->result_class)) {
25 $new->parent_source($source);
26 }
876f6525 27 }
28 return $new;
29}
30
4e4f71e3 31method add_additional_parents (@classes) {
32 foreach my $class (@classes) {
33 Class::C3::Componentised->ensure_class_loaded($class);
34 $self->add_additional_parent(
35 $class->result_source_instance
36 );
37 }
38}
39
803ffff2 40method add_additional_parent ($source) {
41 my ($our_pk, $their_pk) = map {
42 join('|',sort $_->primary_columns)
43 } ($self, $source);
44
45 confess "Can't attach additional parent ${\$source->name} - it has different PKs ($their_pk versus our $our_pk)"
46 unless $their_pk eq $our_pk;
47 $self->additional_parents([
48 @{$self->additional_parents||[]}, $source
49 ]);
50 $self->add_columns(
51 map {
52 $_ => # put the extra key first to default it
53 { originally_defined_in => $source->name, %{$source->column_info($_)}, },
54 } grep !$self->has_column($_), $source->columns
55 );
56 foreach my $rel ($source->relationships) {
57 my $rel_info = $source->relationship_info($rel);
58 $self->add_relationship(
59 $rel, $rel_info->{source}, $rel_info->{cond},
60 # extra key first to default it
61 {originally_defined_in => $source->name, %{$rel_info->{attrs}}},
62 );
63 }
a010ebf9 64 { no strict 'refs';
65 push(@{$self->result_class.'::ISA'}, $source->result_class);
66 }
803ffff2 67}
68
8b229aa6 69method _source_by_name ($name) {
70 my $schema = $self->schema;
71 my ($source) =
72 grep { $_->name eq $name }
73 map $schema->source($_), $schema->sources;
74 confess "Couldn't find attached source for parent $name - did you use load_classes? This module is only compatible with load_namespaces"
75 unless $source;
76 return $source;
77}
78
7abe3af2 79method schema (@args) {
80 my $ret = $self->next::method(@args);
81 if (@args) {
c73d582b 82 if ($self->parent_source) {
c73d582b 83 my $parent_name = $self->parent_source->name;
8b229aa6 84 $self->parent_source($self->_source_by_name($parent_name));
c73d582b 85 }
8b229aa6 86 $self->additional_parents([
87 map { $self->_source_by_name($_->name) }
88 @{$self->additional_parents||[]}
89 ]);
7abe3af2 90 }
91 return $ret;
92}
93
c73d582b 94method attach_additional_sources () {
4d88a8d7 95 my $raw_name = $self->raw_source_name;
ca79850d 96 my $schema = $self->schema;
97
98 # if the raw source is already present we can assume we're done
99 return if grep { $_ eq $raw_name } $schema->sources;
4d88a8d7 100
ca79850d 101 # our parent should've been registered already actually due to DBIC
102 # attaching subclass sources later in load_namespaces
4d88a8d7 103
ca79850d 104 my $parent;
105 if ($self->parent_source) {
106 my $parent_name = $self->parent_source->name;
107 ($parent) =
108 grep { $_->name eq $parent_name }
109 map $schema->source($_), $schema->sources;
110 confess "Couldn't find attached source for parent $parent_name - did you use load_classes? This module is only compatible with load_namespaces"
111 unless $parent;
05fd2477 112 $self->parent_source($parent); # so our parent is the one in this schema
ca79850d 113 }
4d88a8d7 114
115 # create the raw table source
116
117 my $table = Table->new({ name => $self->raw_table_name });
118
ca79850d 119 # we don't need to add the PK cols explicitly if we're the root table
4d88a8d7 120 # since they'll get added below
121
803ffff2 122 my %pk_join;
123
ca79850d 124 if ($parent) {
ca79850d 125 foreach my $pri ($self->primary_columns) {
126 my %info = %{$self->column_info($pri)};
127 delete @info{qw(is_auto_increment sequence auto_nextval)};
7abe3af2 128 $table->add_column($pri => \%info);
803ffff2 129 $pk_join{"foreign.${pri}"} = "self.${pri}";
ca79850d 130 }
4d88a8d7 131 # have to use source name lookups rather than result class here
132 # because we don't actually have a result class on the raw sources
803ffff2 133 $table->add_relationship('parent', $parent->raw_source_name, \%pk_join);
134 $self->depends_on->{$parent->source_name} = 1;
135 }
136
137 foreach my $add (@{$self->additional_parents||[]}) {
138 $table->add_relationship(
139 'parent_'.$add->name, $add->source_name, \%pk_join
140 );
141 $self->depends_on->{$add->source_name} = 1;
ca79850d 142 }
4d88a8d7 143
144 # add every column that's actually a concrete part of us
145
146 $table->add_columns(
147 map { ($_ => { %{$self->column_info($_)} }) }
148 grep { $self->column_info($_)->{originally_defined_in} eq $self->name }
149 $self->columns
150 );
ca79850d 151 $table->set_primary_key($self->primary_columns);
490d5481 152
153 # we need to copy our rels to the raw object as well
154 # note that ->add_relationship on a source object doesn't create an
155 # accessor so we can leave that part in the attributes
156
157 # if the other side is a table then we need to copy any rels it has
158 # back to us, as well, so that they point at the raw table. if the
159 # other side is an MTI view then we need to create the rels to it to
160 # point at -its- raw table; we don't need to worry about backrels because
161 # it's going to run this method too (and its raw source might not exist
162 # yet so we can't, anyway)
163
164 foreach my $rel ($self->relationships) {
165 my $rel_info = $self->relationship_info($rel);
166
803ffff2 167 # if we got this from the superclass, -its- raw table will nail this.
168 # if we got it from an additional parent, it's its problem.
169 next unless $rel_info->{attrs}{originally_defined_in} eq $self->name;
170
490d5481 171 my $f_source = $schema->source($rel_info->{source});
172
173 # __PACKAGE__ is correct here because subclasses should be caught
174
175 my $one_of_us = $f_source->isa(__PACKAGE__);
176
177 my $f_source_name = $f_source->${\
178 ($one_of_us ? 'raw_source_name' : 'source_name')
179 };
180
181 $table->add_relationship(
182 '_'.$rel, $f_source_name, @{$rel_info}{qw(cond attrs)}
183 );
184
185 unless ($one_of_us) {
186 my $reverse = do {
187 # we haven't been registered yet, so reverse_ cries
188 # XXX this is evil and will probably break eventually
189 local @{$schema->source_registrations}
190 {map $self->$_, qw(source_name result_class)}
191 = ($self, $self);
192 $self->reverse_relationship_info($rel);
193 };
194 foreach my $rev_rel (keys %$reverse) {
195 $f_source->add_relationship(
196 '_raw_'.$rev_rel, $raw_name, @{$reverse->{$rev_rel}}{qw(cond attrs)}
197 );
198 }
199 }
200 }
201
ca79850d 202 $schema->register_source($raw_name => $table);
203}
204
205method set_primary_key (@args) {
206 if ($self->parent_source) {
207 confess "Can't set primary key on a subclass";
208 }
209 return $self->next::method(@args);
876f6525 210}
211
4d88a8d7 212method raw_source_name () {
876f6525 213 my $base = $self->source_name;
05fd2477 214 confess "Can't generate raw source name for ${\$self->name} when we don't have a source_name"
876f6525 215 unless $base;
216 return 'Raw::'.$base;
217}
70d56286 218
4d88a8d7 219method raw_table_name () {
220 return '_'.$self->name;
221}
222
876f6525 223method add_columns (@args) {
224 my $ret = $self->next::method(@args);
225 $_->{originally_defined_in} ||= $self->name for values %{$self->_columns};
226 return $ret;
70d56286 227}
228
803ffff2 229method add_relationship ($name, $f_source, $cond, $attrs) {
230 $self->next::method(
231 $name, $f_source, $cond,
232 { originally_defined_in => $self->name, %{$attrs||{}}, }
233 );
234}
235
487f4489 236BEGIN {
237
238 # helper routines, constructed as anon subs so autoclean nukes them
239
240 use signatures;
241
242 *argify = sub (@names) {
243 map '_'.$_, @names;
244 };
245
246 *qualify_with = sub ($source, @names) {
92ebfc06 247 my $name = blessed($source) ? $source->name : $source;
248 map join('.', $name, $_), @names;
487f4489 249 };
250
251 *body_cols = sub ($source) {
252 my %pk; @pk{$source->primary_columns} = ();
253 map +{ %{$source->column_info($_)}, name => $_ },
254 grep !exists $pk{$_}, $source->columns;
255 };
256
257 *pk_cols = sub ($source) {
258 map +{ %{$source->column_info($_)}, name => $_ },
259 $source->primary_columns;
260 };
261
92ebfc06 262 *names_of = sub (@cols) { map $_->{name}, @cols };
487f4489 263
05fd2477 264 *function_body = sub ($name, $args, $body_parts) {
265 my $arglist = join(
266 ', ',
388d83fc 267 map "_${\$_->{name}} ${\uc($_->{data_type})}",
05fd2477 268 @$args
269 );
270 my $body = join("\n", '', map " $_;", @$body_parts);
271 return strip tt q{
272 CREATE OR REPLACE FUNCTION [% name %]
273 ([% arglist %])
274 RETURNS VOID AS $function$
275 BEGIN
276 [%- body %]
277 END;
278 $function$ LANGUAGE plpgsql;
279 };
487f4489 280 };
487f4489 281}
282
05fd2477 283BEGIN {
284
285 use signatures;
286
287 *arg_hash = sub ($source) {
288 map +($_ => \(argify $_)), names_of body_cols $source;
289 };
92ebfc06 290
291 *rule_body = sub ($on, $to, $oldlist, $newlist) {
292 my $arglist = join(', ',
293 (qualify_with 'OLD', names_of @$oldlist),
294 (qualify_with 'NEW', names_of @$newlist),
295 );
296 $to = $to->name if blessed($to);
297 return strip tt q{
298 CREATE RULE _[% to %]_[% on %]_rule AS
299 ON [% on | upper %] TO [% to %]
300 DO INSTEAD (
3c259cfb 301 SELECT [% to %]_[% on %]([% arglist %])
92ebfc06 302 );
303 };
304 };
05fd2477 305}
306
307method root_table () {
308 $self->parent_source
309 ? $self->parent_source->root_table
310 : $self->schema->source($self->raw_source_name)
311}
312
487f4489 313method view_definition () {
314 my $schema = $self->schema;
315 confess "Can't generate view without connected schema, sorry"
316 unless $schema && $schema->storage;
317 my $sqla = $schema->storage->sql_maker;
2816c8ed 318 my $table = $self->schema->source($self->raw_source_name);
487f4489 319 my $super_view = $self->parent_source;
2816c8ed 320 my @all_parents = my @other_parents = @{$self->additional_parents||[]};
321 push(@all_parents, $super_view) if defined($super_view);
322 my @sources = ($table, @all_parents);
487f4489 323 my @body_cols = map body_cols($_), @sources;
324 my @pk_cols = pk_cols $self;
92ebfc06 325
326 # SELECT statement
327
2816c8ed 328 my $am_root = !($super_view || @other_parents);
329
487f4489 330 my $select = $sqla->select(
2816c8ed 331 ($am_root
332 ? ($table->name)
333 : ([ # FROM _tbl _tbl
487f4489 334 { $table->name => $table->name },
2816c8ed 335 map {
336 my $parent = $_;
337 [ # JOIN view view
338 { $parent->name => $parent->name },
339 # ON _tbl.id = view.id
340 { map +(qualify_with($parent, $_), qualify_with($table, $_)),
341 names_of @pk_cols }
342 ]
343 } @all_parents
487f4489 344 ])
2816c8ed 345 ),
487f4489 346 [ (qualify_with $table, names_of @pk_cols), names_of @body_cols ],
05fd2477 347 ).';';
92ebfc06 348
2816c8ed 349 my ($now, @next) = grep defined, $super_view, $table, @other_parents;
92ebfc06 350
351 # INSERT function
352
05fd2477 353 # NOTE: this assumes a single PK col called id with a sequence somewhere
354 # but nothing else -should- so fixing this should make everything work
355 my $insert_func =
356 function_body
357 $self->name.'_insert',
358 \@body_cols,
359 [
2816c8ed 360 $sqla->insert( # INSERT INTO tbl/super_view (foo, ...) VALUES (_foo, ...)
05fd2477 361 $now->name,
362 { arg_hash $now },
363 ),
2816c8ed 364 (map {
365 $sqla->insert( # INSERT INTO parent (id, ...)
366 # VALUES (currval('_root_tbl_id_seq'), ...)
367 $_->name,
368 {
369 (arg_hash $_),
370 id => \"currval('${\$self->root_table->name}_id_seq')",
371 }
372 )
373 } @next)
05fd2477 374 ];
92ebfc06 375
05fd2477 376 # note - similar to arg_hash but not quite enough to share code sanely
377 my $pk_where = { # id = _id AND id2 = _id2 ...
378 map +($_ => \"= ${\argify $_}"), names_of @pk_cols
379 };
92ebfc06 380
381 # UPDATE function
382
05fd2477 383 my $update_func =
384 function_body
385 $self->name.'_update',
386 [ @pk_cols, @body_cols ],
387 [ map $sqla->update(
388 $_->name, # UPDATE foo
389 { arg_hash $_ }, # SET a = _a
390 $pk_where,
391 ), @sources
392 ];
92ebfc06 393
394 # DELETE function
395
05fd2477 396 my $delete_func =
397 function_body
398 $self->name.'_delete',
399 [ @pk_cols ],
400 [ map $sqla->delete($_->name, $pk_where), @sources ];
92ebfc06 401
402 my @rules = (
403 (rule_body insert => $self, [], \@body_cols),
404 (rule_body update => $self, \@pk_cols, \@body_cols),
405 (rule_body delete => $self, \@pk_cols, []),
406 );
407 return join("\n\n", $select, $insert_func, $update_func, $delete_func, @rules);
487f4489 408}
409
70d56286 4101;
146ec120 411
412__END__
413
414# how this works:
415#
416# On construction, we hook $self->result_class->result_source_instance
417# if present to get the superclass' source object
418#
419# When attached to a schema, we need to add sources to that schema with
420# appropriate relationships for the foreign keys so the concrete tables
421# get generated
422#
423# We also generate our own view definition using this class' concrete table
424# and the view for the superclass, and stored procedures for the insert,
425# update and delete operations on this view.
426#
427# deploying the postgres rules through SQLT may be a pain though.
428
429=encoding utf-8
430
431=head1 NAME
432
433DBIx::Class::ResultSource::MultipleTableInheritance -- Use multiple tables to define your classes
434
435=head1 SYNOPSIS
436
437
438 {
439 package MyApp::Schema::Result::Coffee;
440
441 __PACKAGE__->table_class('DBIx::Class::ResultSource::MultipleTableInheritance');
442 __PACKAGE__->table('coffee');
443 __PACKAGE__->add_columns(
444 "id",
445 {
446 data_type => "integer",
447 default_value => "nextval('coffee_seq'::regclass)",
448 is_auto_increment => 1,
449 is_foreign_key => 1,
450 is_nullable => 0,
451 size => 4,
452 },
453 "flavor",
454 {
455 data_type => "text",
456 default_value => "good",
457 },
458 );
459
460 __PACKAGE__->set_primary_key("id");
461
462 1;
463 }
464
465 {
466 package MyApp::Schema::Result::Sumatra;
467
468 use parent 'Coffee';
469
470 __PACKAGE__->table('sumatra');
471
472 __PACKAGE__->add_columns(
473 "aroma",
474 {
475 data_type => "text",
476 default_value => undef,
477 is_nullable => 0,
478 },
479 );
480
481 1;
482 }
483
484 ...
485
486 my $schema = MyApp::Schema->connect($dsn);
487
488 my $cup = $schema->resultset('Sumatra')->new;
489
490 print STDERR Dumper $cup->columns;
491
492 $VAR1 = 'id';
493 $VAR2 = 'flavor';
494 $VAR3 = 'aroma';
495
496
497
498Inherit from this package and you can make a resultset class from a view, but that's more than a little bit misleading: the result is B<transparentlt writable>.
499
500This is accomplished through the use of stored functions that map changes written to the view to changes to the underlying concrete tables.
501
502=head1 WHY?
503
504In many applications, many classes are subclasses of others. Let's say you have this schema:
505
506 # Conceptual domain model
507
508 class User {
509 has id,
510 has name,
511 has password
512 }
513
514 class Investor {
515 has id,
516 has name,
517 has password,
518 has dollars
519 }
520
521That's redundant. Hold on a sec...
522
523 class User {
524 has id,
525 has name,
526 has password
527 }
528
529 class Investor isa User {
530 has dollars
531 }
532
533Good idea, but how to put this into code?
534
535One far-too common and absolutely horrendous solution is to have a "checkbox" in your database: a nullable "investor" column, which entails a nullable "dollars" column, in the user table.
536
537 create table "user" (
538 "id" integer not null primary key autoincrement,
539 "name" text not null,
540 "password" text not null,
541 "investor" tinyint(1),
542 "dollars" integer
543 );
544
545Let's not discuss that further.
546
547A second, better, solution is to break out the two tables into user and investor:
548
549 create table "user" (
550 "id" integer not null primary key autoincrement,
551 "name" text not null,
552 "password" text not null
553 );
554
555 create table "investor" (
556 "id" integer not null references user("id"),
557 "dollars" integer
558 );
559
560So that investor's PK is just an FK to the user. We can clearly see the class hierarchy here, in which investor is a subclass of user. In DBIx::Class applications, this second strategy looks like:
561
562 my $user_rs = $schema->resultset('User');
563 my $new_user = $user_rs->create(
564 name => $args->{name},
565 password => $args->{password},
566 );
567
568 ...
569
570 my $new_investor = $schema->resultset('Investor')->create(
571 id => $new_user->id,
572 dollars => $args->{dollars},
573 );
574
575One can cope well with the second strategy, and it seems to be the most popular smart choice.
576
577=head1 HOW?
578
579There is a third strategy implemented here. Make the database do more of the work. It'll save us some typing and it'll make for more expressive code. What if we could do this:
580
581 my $new_investor = $schema->resultset('Investor')->create(
582 name => $args->{name},
583 password => $args->{password},
584 dollars => $args->{dollars},
585 );
586
587And have it Just Work? The user ( {name => $args->{name}, password => $args->{password} } ) should be created transparently, and the use of either user or investor in your code should require no special handling. Deleting and updating $new_investor should also delete or update the user row.
588
589It does. User and investor are both views, their concrete tables abstracted away behind a set of rules and triggers. You would expect the above DBIC create statement to look like this in SQL:
590
591 INSERT INTO investor ("name","password","dollars") VALUES (...);
592
593But using MTI, it is really this:
594
595 INSERT INTO _user_table ("username","password") VALUES (...);
596 INSERT INTO _investor_table ("id","dollars") VALUES (currval('_user_table_id_seq',...) );
597
598For deletes, the triggers fire in reverse, to preserve referential integrity (foreign key constraints). For instance:
599
600 my $investor = $schema->resultset('Investor')->find({id => $args->{id}});
601 $investor->delete;
602
603Becomes:
604
605 DELETE FROM _investor_table WHERE ("id" = ?);
606 DELETE FROM _user_table WHERE ("id" = ?);
607
608
609
610
611
612
613
614
615=head1 AUTHOR
616
617Matt S. Trout, E<lt>mst@shadowcatsystems.co.ukE<gt>
618
619=head2 CONTRIBUTORS
620
621Docs: Amiri Barksdale, E<lt>amiri@metalabel.comE<gt>
622
623=head1 LICENSE
624
625This library is free software; you can redistribute it and/or modify
626it under the same terms as Perl itself.
627
628=head1 SEE ALSO
629
630L<DBIx::Class>
631L<DBIx::Class::ResultSource>
632
633=cut