Commit | Line | Data |
146ec120 |
1 | NAME |
2 | DBIx::Class::ResultSource::MultipleTableInheritance -- Use multiple |
3 | tables to define your classes |
4 | |
5 | SYNOPSIS |
6 | { |
7 | package MyApp::Schema::Result::Coffee; |
8 | |
9 | __PACKAGE__->table_class('DBIx::Class::ResultSource::MultipleTableInheritance'); |
10 | __PACKAGE__->table('coffee'); |
11 | __PACKAGE__->add_columns( |
12 | "id", |
13 | { |
14 | data_type => "integer", |
15 | default_value => "nextval('coffee_seq'::regclass)", |
16 | is_auto_increment => 1, |
17 | is_foreign_key => 1, |
18 | is_nullable => 0, |
19 | size => 4, |
20 | }, |
21 | "flavor", |
22 | { |
23 | data_type => "text", |
24 | default_value => "good", |
25 | }, |
26 | ); |
27 | |
28 | __PACKAGE__->set_primary_key("id"); |
29 | |
30 | 1; |
31 | } |
32 | |
33 | { |
34 | package MyApp::Schema::Result::Sumatra; |
35 | |
36 | use parent 'Coffee'; |
37 | |
38 | __PACKAGE__->table('sumatra'); |
39 | |
40 | __PACKAGE__->add_columns( |
41 | "aroma", |
42 | { |
43 | data_type => "text", |
44 | default_value => undef, |
45 | is_nullable => 0, |
46 | }, |
47 | ); |
48 | |
49 | 1; |
50 | } |
51 | |
52 | ... |
53 | |
54 | my $schema = MyApp::Schema->connect($dsn); |
55 | |
56 | my $cup = $schema->resultset('Sumatra')->new; |
57 | |
58 | print STDERR Dumper $cup->columns; |
59 | |
60 | $VAR1 = 'id'; |
61 | $VAR2 = 'flavor'; |
62 | $VAR3 = 'aroma'; |
63 | |
64 | Inherit from this package and you can make a resultset class from a |
65 | view, but that's more than a little bit misleading: the result is |
66 | writable and updateable--transparently. |
67 | |
68 | This is accomplished through the use of stored functions that map |
69 | changes written to the view to changes to the underlying concrete |
70 | tables. |
71 | |
72 | WHY? |
73 | In many applications, many classes are subclasses of others. Let's say |
74 | you have this schema: |
75 | |
76 | # Conceptual domain model |
77 | |
78 | class User { |
79 | has id, |
80 | has name, |
81 | has password |
82 | } |
83 | |
84 | class Investor { |
85 | has id, |
86 | has name, |
87 | has password, |
88 | has dollars |
89 | } |
90 | |
91 | That's redundant. Hold on a sec... |
92 | |
93 | class User { |
94 | has id, |
95 | has name, |
96 | has password |
97 | } |
98 | |
99 | class Investor isa User { |
100 | has dollars |
101 | } |
102 | |
103 | Good idea, but how to put this into code? |
104 | |
105 | One far-too common and absolutely horrendous solution is to have a |
106 | "checkbox" in your database: a nullable "investor" column, which entails |
107 | a nullable "dollars" column, in the user table. |
108 | |
109 | create table "user" ( |
110 | "id" integer not null primary key autoincrement, |
111 | "name" text not null, |
112 | "password" text not null, |
113 | "investor" tinyint(1), |
114 | "dollars" integer |
115 | ); |
116 | |
117 | Let's not discuss that further. |
118 | |
119 | A second, better, solution is to break out the two tables into user and |
120 | investor: |
121 | |
122 | create table "user" ( |
123 | "id" integer not null primary key autoincrement, |
124 | "name" text not null, |
125 | "password" text not null |
126 | ); |
127 | |
128 | create table "investor" ( |
129 | "id" integer not null references user("id"), |
130 | "dollars" integer |
131 | ); |
132 | |
133 | So that investor's PK is just an FK to the user. We can clearly see the |
134 | class hierarchy here, in which investor is a subclass of user. In |
135 | DBIx::Class applications, this second strategy looks like: |
136 | |
137 | my $user_rs = $schema->resultset('User'); |
138 | my $new_user = $user_rs->create( |
139 | name => $args->{name}, |
140 | password => $args->{password}, |
141 | ); |
142 | |
143 | ... |
144 | |
145 | my $new_investor = $schema->resultset('Investor')->create( |
146 | id => $new_user->id, |
147 | dollars => $args->{dollars}, |
148 | ); |
149 | |
150 | One can cope well with the second strategy, and it seems to be the most |
151 | popular smart choice. |
152 | |
153 | HOW? |
154 | There is a third strategy implemented here. Make the database do more of |
155 | the work. It'll save us some typing and it'll make for more expressive |
156 | code. What if we could do this: |
157 | |
158 | my $new_investor = $schema->resultset('Investor')->create( |
159 | name => $args->{name}, |
160 | password => $args->{password}, |
161 | dollars => $args->{dollars}, |
162 | ); |
163 | |
164 | And have it Just Work? The user ( {name => $args->{name}, password => |
165 | $args->{password} } ) should be created transparently, and the use of |
166 | either user or investor in your code should require no special handling. |
167 | Deleting and updating $new_investor should also delete or update the |
168 | user row. |
169 | |
170 | It does. User and investor are both views, their concrete tables |
171 | abstracted away behind a set of rules and triggers. You would expect the |
172 | above DBIC create statement to look like this in SQL: |
173 | |
174 | INSERT INTO investor ("name","password","dollars") VALUES (...); |
175 | |
176 | But using MTI, it is really this: |
177 | |
178 | INSERT INTO _user_table ("username","password") VALUES (...); |
179 | INSERT INTO _investor_table ("id","dollars") VALUES (currval('_user_table_id_seq',...) ); |
180 | |
181 | For deletes, the triggers fire in reverse, to preserve referential |
182 | integrity (foreign key constraints). For instance: |
183 | |
184 | my $investor = $schema->resultset('Investor')->find({id => $args->{id}}); |
185 | $investor->delete; |
186 | |
187 | Becomes: |
188 | |
189 | DELETE FROM _investor_table WHERE ("id" = ?); |
190 | DELETE FROM _user_table WHERE ("id" = ?); |
191 | |
192 | AUTHOR |
193 | Matt S. Trout, <mst@shadowcatsystems.co.uk> |
194 | |
195 | CONTRIBUTORS |
196 | Docs: Amiri Barksdale, <amiri@metalabel.com> |
197 | |
198 | LICENSE |
199 | This library is free software; you can redistribute it and/or modify it |
200 | under the same terms as Perl itself. |
201 | |
202 | SEE ALSO |
203 | DBIx::Class DBIx::Class::ResultSource |
204 | |