Beginning of docs. Making headway. Next is subroutine docs
[dbsrgits/DBIx-Class-ResultSource-MultipleTableInheritance.git] / lib / DBIx / Class / ResultSource / MultipleTableInheritance.pm
index f05b19b..a7cf799 100644 (file)
@@ -11,20 +11,7 @@ use String::TT qw(strip tt);
 use Scalar::Util qw(blessed);
 use namespace::autoclean;
 
-# how this works:
-#
-# On construction, we hook $self->result_class->result_source_instance
-# if present to get the superclass' source object
-# 
-# When attached to a schema, we need to add sources to that schema with
-# appropriate relationships for the foreign keys so the concrete tables
-# get generated
-#
-# We also generate our own view definition using this class' concrete table
-# and the view for the superclass, and stored procedures for the insert,
-# update and delete operations on this view.
-#
-# deploying the postgres rules through SQLT may be a pain though.
+our $VERSION = 0.01;
 
 __PACKAGE__->mk_group_accessors(simple => qw(parent_source additional_parents));
 
@@ -421,3 +408,226 @@ method view_definition () {
 }
 
 1;
+
+__END__
+
+# how this works:
+#
+# On construction, we hook $self->result_class->result_source_instance
+# if present to get the superclass' source object
+# 
+# When attached to a schema, we need to add sources to that schema with
+# appropriate relationships for the foreign keys so the concrete tables
+# get generated
+#
+# We also generate our own view definition using this class' concrete table
+# and the view for the superclass, and stored procedures for the insert,
+# update and delete operations on this view.
+#
+# deploying the postgres rules through SQLT may be a pain though.
+
+=encoding utf-8
+
+=head1 NAME
+
+DBIx::Class::ResultSource::MultipleTableInheritance -- Use multiple tables to define your classes 
+
+=head1 SYNOPSIS
+
+
+    {
+        package MyApp::Schema::Result::Coffee;
+
+        __PACKAGE__->table_class('DBIx::Class::ResultSource::MultipleTableInheritance');
+        __PACKAGE__->table('coffee');
+        __PACKAGE__->add_columns(
+          "id",
+          {
+            data_type => "integer",
+            default_value => "nextval('coffee_seq'::regclass)",
+            is_auto_increment => 1,
+            is_foreign_key => 1,
+            is_nullable => 0,
+            size => 4,
+          },
+          "flavor",
+          {
+            data_type => "text",
+            default_value => "good",
+          },
+        );
+
+        __PACKAGE__->set_primary_key("id");
+
+        1;
+    }
+
+    {
+        package MyApp::Schema::Result::Sumatra;
+
+        use parent 'Coffee';
+
+        __PACKAGE__->table('sumatra');
+
+        __PACKAGE__->add_columns(
+          "aroma",
+          {
+            data_type => "text",
+            default_value => undef,
+            is_nullable => 0,
+          },
+        );
+
+        1;
+    }
+    
+    ...
+
+    my $schema = MyApp::Schema->connect($dsn);
+
+    my $cup = $schema->resultset('Sumatra')->new;
+
+    print STDERR Dumper $cup->columns;
+
+        $VAR1 = 'id';
+        $VAR2 = 'flavor';
+        $VAR3 = 'aroma';
+
+
+
+Inherit 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>.
+
+This is accomplished through the use of stored functions that map changes written to the view to changes to the underlying concrete tables.
+
+=head1 WHY?
+
+In many applications, many classes are subclasses of others. Let's say you have this schema:
+
+    # Conceptual domain model
+    
+    class User {
+            has id,
+            has name,
+            has password
+    }
+
+    class Investor {
+        has id,
+        has name,
+        has password,
+        has dollars
+    }
+
+That's redundant. Hold on a sec...
+
+    class User {
+            has id,
+            has name,
+            has password
+    }
+
+    class Investor isa User {
+        has dollars
+    }
+
+Good idea, but how to put this into code?
+
+One 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.
+
+    create table "user" (
+        "id" integer not null primary key autoincrement,
+        "name" text not null,
+        "password" text not null,
+        "investor" tinyint(1),
+        "dollars" integer
+    );
+
+Let's not discuss that further.
+
+A second, better, solution is to break out the two tables into user and investor:
+
+    create table "user" (
+        "id" integer not null primary key autoincrement,
+        "name" text not null,
+        "password" text not null
+    );
+        
+    create table "investor" (
+        "id" integer not null references user("id"),
+        "dollars" integer
+    );
+
+So 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:
+    
+    my $user_rs = $schema->resultset('User');
+    my $new_user = $user_rs->create(
+        name => $args->{name},
+        password => $args->{password},
+    );
+
+    ...
+
+    my $new_investor = $schema->resultset('Investor')->create(
+        id => $new_user->id,
+        dollars => $args->{dollars},
+    );
+
+One can cope well with the second strategy, and it seems to be the most popular smart choice.
+
+=head1 HOW?
+
+There 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:
+
+    my $new_investor = $schema->resultset('Investor')->create(
+        name => $args->{name},
+        password => $args->{password},
+        dollars => $args->{dollars},
+    );
+    
+And 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.
+
+It 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:
+
+    INSERT INTO investor ("name","password","dollars") VALUES (...);
+
+But using MTI, it is really this:
+
+    INSERT INTO _user_table ("username","password") VALUES (...);
+    INSERT INTO _investor_table ("id","dollars") VALUES (currval('_user_table_id_seq',...) );
+
+For deletes, the triggers fire in reverse, to preserve referential integrity (foreign key constraints). For instance:
+
+   my $investor = $schema->resultset('Investor')->find({id => $args->{id}});
+   $investor->delete;
+
+Becomes:
+
+    DELETE FROM _investor_table WHERE ("id" = ?);
+    DELETE FROM _user_table WHERE ("id" = ?);
+
+
+    
+
+
+
+
+
+=head1 AUTHOR
+
+Matt S. Trout, E<lt>mst@shadowcatsystems.co.ukE<gt>
+
+=head2 CONTRIBUTORS
+
+Docs: Amiri Barksdale, E<lt>amiri@metalabel.comE<gt>
+
+=head1 LICENSE
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself.
+
+=head1 SEE ALSO
+
+L<DBIx::Class>
+L<DBIx::Class::ResultSource>
+
+=cut