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 |
e7189506 |
66 | transparently writable. |
146ec120 |
67 | |
e7189506 |
68 | This is accomplished through the use of stored procedures that map |
146ec120 |
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 | |
e7189506 |
99 | class Investor extends User { |
146ec120 |
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 |
e7189506 |
155 | the work: hide the nasty bits so we don't have to handle them unless we |
156 | really want to. It'll save us some typing and it'll make for more |
157 | expressive code. What if we could do this: |
146ec120 |
158 | |
159 | my $new_investor = $schema->resultset('Investor')->create( |
160 | name => $args->{name}, |
161 | password => $args->{password}, |
162 | dollars => $args->{dollars}, |
163 | ); |
164 | |
e7189506 |
165 | And have it Just Work? The user... |
166 | |
167 | { |
168 | name => $args->{name}, |
169 | password => $args->{password}, |
170 | } |
171 | |
172 | should be created behind the scenes, and the use of either user or |
173 | investor in your code should require no special handling. Deleting and |
174 | updating $new_investor should also delete or update the user row. |
146ec120 |
175 | |
176 | It does. User and investor are both views, their concrete tables |
177 | abstracted away behind a set of rules and triggers. You would expect the |
178 | above DBIC create statement to look like this in SQL: |
179 | |
180 | INSERT INTO investor ("name","password","dollars") VALUES (...); |
181 | |
182 | But using MTI, it is really this: |
183 | |
184 | INSERT INTO _user_table ("username","password") VALUES (...); |
185 | INSERT INTO _investor_table ("id","dollars") VALUES (currval('_user_table_id_seq',...) ); |
186 | |
187 | For deletes, the triggers fire in reverse, to preserve referential |
188 | integrity (foreign key constraints). For instance: |
189 | |
190 | my $investor = $schema->resultset('Investor')->find({id => $args->{id}}); |
191 | $investor->delete; |
192 | |
193 | Becomes: |
194 | |
195 | DELETE FROM _investor_table WHERE ("id" = ?); |
196 | DELETE FROM _user_table WHERE ("id" = ?); |
197 | |
e7189506 |
198 | METHODS |
199 | new MTI find the parents, if any, of your resultset class and adds them |
200 | to the list of parent_sources for the table. |
201 | |
202 | add_additional_parents |
203 | Continuing with coffee: |
204 | |
205 | __PACKAGE__->result_source_instance->add_additional_parents( |
206 | qw/ |
207 | MyApp::Schema::Result::Beverage |
208 | MyApp::Schema::Result::Liquid |
209 | / |
210 | ); |
211 | |
212 | This just lets you manually add additional parents beyond the ones |
213 | MTI finds. |
214 | |
215 | add_additional_parent |
216 | __PACKAGE__->result_source_instance->add_additional_parent( |
217 | MyApp::Schema::Result::Beverage |
218 | ); |
219 | |
220 | You can also add just one. |
221 | |
222 | attach_additional_sources |
223 | MTI takes the parents' sources and relationships, creates new |
224 | DBIx::Class:Table object from them, and registers this as a new, |
225 | raw, source in the schema, e.g., |
226 | |
227 | use MyApp::Schema; |
228 | |
229 | print STDERR map { "$_\n" } MyApp::Schema->sources; |
230 | |
231 | # Coffee |
232 | # Beverage |
233 | # Liquid |
234 | # Sumatra |
235 | # Raw::Sumatra |
236 | |
237 | Raw::Sumatra will be used to generate the view. |
238 | |
239 | view_definition |
240 | This takes the raw table and generates the view (and stored |
241 | procedures) you will use. |
242 | |
146ec120 |
243 | AUTHOR |
244 | Matt S. Trout, <mst@shadowcatsystems.co.uk> |
245 | |
246 | CONTRIBUTORS |
247 | Docs: Amiri Barksdale, <amiri@metalabel.com> |
248 | |
249 | LICENSE |
250 | This library is free software; you can redistribute it and/or modify it |
251 | under the same terms as Perl itself. |
252 | |
253 | SEE ALSO |
254 | DBIx::Class DBIx::Class::ResultSource |
255 | |