Commit | Line | Data |
fbbb9084 |
1 | =head1 NAME |
d442cc9f |
2 | |
4b4d3884 |
3 | Catalyst::Manual::Tutorial::BasicCRUD - Catalyst Tutorial - Chapter 4: Basic CRUD |
d442cc9f |
4 | |
5 | |
6 | =head1 OVERVIEW |
7 | |
4b4d3884 |
8 | This is B<Chapter 4 of 10> for the Catalyst tutorial. |
d442cc9f |
9 | |
10 | L<Tutorial Overview|Catalyst::Manual::Tutorial> |
11 | |
12 | =over 4 |
13 | |
14 | =item 1 |
15 | |
16 | L<Introduction|Catalyst::Manual::Tutorial::Intro> |
17 | |
18 | =item 2 |
19 | |
20 | L<Catalyst Basics|Catalyst::Manual::Tutorial::CatalystBasics> |
21 | |
22 | =item 3 |
23 | |
3533daff |
24 | L<More Catalyst Basics|Catalyst::Manual::Tutorial::MoreCatalystBasics> |
d442cc9f |
25 | |
26 | =item 4 |
27 | |
3533daff |
28 | B<Basic CRUD> |
d442cc9f |
29 | |
30 | =item 5 |
31 | |
3533daff |
32 | L<Authentication|Catalyst::Manual::Tutorial::Authentication> |
d442cc9f |
33 | |
34 | =item 6 |
35 | |
3533daff |
36 | L<Authorization|Catalyst::Manual::Tutorial::Authorization> |
d442cc9f |
37 | |
38 | =item 7 |
39 | |
3533daff |
40 | L<Debugging|Catalyst::Manual::Tutorial::Debugging> |
d442cc9f |
41 | |
42 | =item 8 |
43 | |
3533daff |
44 | L<Testing|Catalyst::Manual::Tutorial::Testing> |
d442cc9f |
45 | |
46 | =item 9 |
47 | |
3533daff |
48 | L<Advanced CRUD|Catalyst::Manual::Tutorial::AdvancedCRUD> |
49 | |
50 | =item 10 |
51 | |
d442cc9f |
52 | L<Appendices|Catalyst::Manual::Tutorial::Appendices> |
53 | |
54 | =back |
55 | |
56 | |
d442cc9f |
57 | =head1 DESCRIPTION |
58 | |
4b4d3884 |
59 | This chapter of the tutorial builds on the fairly primitive |
60 | application created in Chapter 3 to add basic support for Create, |
61 | Read, Update, and Delete (CRUD) of C<Book> objects. Note that the |
62 | 'list' function in Chapter 2 already implements the Read portion of |
63 | CRUD (although Read normally refers to reading a single object; you |
72609296 |
64 | could implement full Read functionality using the techniques |
4b4d3884 |
65 | introduced below). This section will focus on the Create and Delete |
66 | aspects of CRUD. More advanced capabilities, including full Update |
67 | functionality, will be addressed in Chapter 9. |
68 | |
69 | Although this chapter of the tutorial will show you how to build CRUD |
70 | functionality yourself, another option is to use a "CRUD builder" type |
71 | of tool to automate the process. You get less control, but it's quick |
72 | and easy. For example, see |
73 | L<CatalystX::ListFramework::Builder|CatalystX::ListFramework::Builder>, |
74 | L<CatalystX::CRUD|CatalystX::CRUD>, and |
7edc5484 |
75 | L<CatalystX::CRUD::YUI|CatalystX::CRUD::YUI>. |
1390ef0e |
76 | |
72609296 |
77 | You can check out the source code for this example from the Catalyst |
78 | Subversion repository as per the instructions in |
1390ef0e |
79 | L<Catalyst::Manual::Tutorial::Intro|Catalyst::Manual::Tutorial::Intro>. |
d442cc9f |
80 | |
3533daff |
81 | |
d442cc9f |
82 | =head1 FORMLESS SUBMISSION |
83 | |
55490817 |
84 | Our initial attempt at object creation will utilize the "URL |
3533daff |
85 | arguments" feature of Catalyst (we will employ the more common form- |
86 | based submission in the sections that follow). |
d442cc9f |
87 | |
88 | |
89 | =head2 Include a Create Action in the Books Controller |
90 | |
91 | Edit C<lib/MyApp/Controller/Books.pm> and enter the following method: |
92 | |
93 | =head2 url_create |
55490817 |
94 | |
d442cc9f |
95 | Create a book with the supplied title, rating, and author |
55490817 |
96 | |
d442cc9f |
97 | =cut |
55490817 |
98 | |
d442cc9f |
99 | sub url_create : Local { |
55490817 |
100 | # In addition to self & context, get the title, rating, & |
101 | # author_id args from the URL. Note that Catalyst automatically |
102 | # puts extra information after the "/<controller_name>/<action_name/" |
d442cc9f |
103 | # into @_ |
104 | my ($self, $c, $title, $rating, $author_id) = @_; |
55490817 |
105 | |
106 | # Call create() on the book model object. Pass the table |
d442cc9f |
107 | # columns/field values we want to set as hash values |
d0496197 |
108 | my $book = $c->model('DB::Books')->create({ |
d442cc9f |
109 | title => $title, |
110 | rating => $rating |
111 | }); |
55490817 |
112 | |
113 | # Add a record to the join table for this book, mapping to |
d442cc9f |
114 | # appropriate author |
115 | $book->add_to_book_authors({author_id => $author_id}); |
116 | # Note: Above is a shortcut for this: |
117 | # $book->create_related('book_authors', {author_id => $author_id}); |
55490817 |
118 | |
d442cc9f |
119 | # Assign the Book object to the stash for display in the view |
120 | $c->stash->{book} = $book; |
55490817 |
121 | |
d442cc9f |
122 | # Set the TT template to use |
123 | $c->stash->{template} = 'books/create_done.tt2'; |
124 | } |
125 | |
126 | Notice that Catalyst takes "extra slash-separated information" from the |
127 | URL and passes it as arguments in C<@_>. The C<url_create> action then |
128 | uses a simple call to the DBIC C<create> method to add the requested |
129 | information to the database (with a separate call to |
130 | C<add_to_book_authors> to update the join table). As do virtually all |
131 | controller methods (at least the ones that directly handle user input), |
132 | it then sets the template that should handle this request. |
133 | |
134 | |
8a472b34 |
135 | =head2 Include a Template for the 'url_create' Action: |
d442cc9f |
136 | |
137 | Edit C<root/src/books/create_done.tt2> and then enter: |
138 | |
139 | [% # Use the TT Dumper plugin to Data::Dumper variables to the browser -%] |
140 | [% # Not a good idea for production use, though. :-) 'Indent=1' is -%] |
141 | [% # optional, but prevents "massive indenting" of deeply nested objects -%] |
142 | [% USE Dumper(Indent=1) -%] |
55490817 |
143 | |
d442cc9f |
144 | [% # Set the page title. META can 'go back' and set values in templates -%] |
145 | [% # that have been processed 'before' this template (here it's for -%] |
72609296 |
146 | [% # root/lib/site/html and root/lib/site/header). Note that META only -%] |
147 | [% # works on simple/static strings (i.e. there is no variable -%] |
148 | [% # interpolation). -%] |
d442cc9f |
149 | [% META title = 'Book Created' %] |
55490817 |
150 | |
d442cc9f |
151 | [% # Output information about the record that was added. First title. -%] |
152 | <p>Added book '[% book.title %]' |
55490817 |
153 | |
d442cc9f |
154 | [% # Output the last name of the first author. This is complicated by an -%] |
155 | [% # issue in TT 2.15 where blessed hash objects are not handled right. -%] |
156 | [% # First, fetch 'book.authors' from the DB once. -%] |
157 | [% authors = book.authors %] |
158 | [% # Now use IF statements to test if 'authors.first' is "working". If so, -%] |
159 | [% # we use it. Otherwise we use a hack that seems to keep TT 2.15 happy. -%] |
55490817 |
160 | by '[% authors.first.last_name IF authors.first; |
d442cc9f |
161 | authors.list.first.value.last_name IF ! authors.first %]' |
55490817 |
162 | |
d442cc9f |
163 | [% # Output the rating for the book that was added -%] |
164 | with a rating of [% book.rating %].</p> |
55490817 |
165 | |
d442cc9f |
166 | [% # Provide a link back to the list page -%] |
167 | [% # 'uri_for()' builds a full URI; e.g., 'http://localhost:3000/books/list' -%] |
8a7c5151 |
168 | <p><a href="[% c.uri_for('/books/list') %]">Return to list</a></p> |
55490817 |
169 | |
d442cc9f |
170 | [% # Try out the TT Dumper (for development only!) -%] |
171 | <pre> |
172 | Dump of the 'book' variable: |
173 | [% Dumper.dump(book) %] |
174 | </pre> |
175 | |
55490817 |
176 | The TT C<USE> directive allows access to a variety of plugin modules |
177 | (TT plugins, that is, not Catalyst plugins) to add extra functionality |
178 | to the base TT capabilities. Here, the plugin allows |
179 | L<Data::Dumper|Data::Dumper> "pretty printing" of objects and |
180 | variables. Other than that, the rest of the code should be familiar |
4b4d3884 |
181 | from the examples in Chapter 3. |
d442cc9f |
182 | |
d442cc9f |
183 | |
8a472b34 |
184 | =head2 Try the 'url_create' Feature |
d442cc9f |
185 | |
186 | If the application is still running from before, use C<Ctrl-C> to kill |
187 | it. Then restart the server: |
188 | |
1390ef0e |
189 | $ DBIC_TRACE=1 script/myapp_server.pl |
d442cc9f |
190 | |
191 | Note that new path for C</books/url_create> appears in the startup debug |
192 | output. |
193 | |
194 | B<TIP>: You can use C<script/myapp_server.pl -r> to have the development |
195 | server auto-detect changed files and reload itself (if your browser acts |
196 | odd, you should also try throwing in a C<-k>). If you make changes to |
197 | the TT templates only, you do not need to reload the development server |
198 | (only changes to "compiled code" such as Controller and Model C<.pm> |
199 | files require a reload). |
200 | |
201 | Next, use your browser to enter the following URL: |
202 | |
203 | http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4 |
204 | |
55490817 |
205 | Your browser should display "Added book 'TCPIP_Illustrated_Vol-2' by |
206 | 'Stevens' with a rating of 5." along with a dump of the new book model |
207 | object as it was returned by DBIC. You should also see the following |
208 | DBIC debug messages displayed in the development server log messages |
fbbb9084 |
209 | if you have DBIC_TRACE set: |
d442cc9f |
210 | |
211 | INSERT INTO books (rating, title) VALUES (?, ?): `5', `TCPIP_Illustrated_Vol-2' |
212 | INSERT INTO book_authors (author_id, book_id) VALUES (?, ?): `4', `6' |
55490817 |
213 | SELECT author.id, author.first_name, author.last_name |
214 | FROM book_authors me JOIN authors author |
d442cc9f |
215 | ON ( author.id = me.author_id ) WHERE ( me.book_id = ? ): '6' |
216 | |
217 | The C<INSERT> statements are obviously adding the book and linking it to |
218 | the existing record for Richard Stevens. The C<SELECT> statement results |
219 | from DBIC automatically fetching the book for the C<Dumper.dump(book)>. |
220 | |
55490817 |
221 | If you then click the "Return to list" link, you should find that |
222 | there are now six books shown (if necessary, Shift+Reload or |
1390ef0e |
223 | Ctrl+Reload your browser at the C</books/list> page). |
d442cc9f |
224 | |
d442cc9f |
225 | |
89d3dae9 |
226 | =head1 CONVERT TO A CHAINED ACTION |
227 | |
55490817 |
228 | Although the example above uses the same C<Local> action type for the |
4b4d3884 |
229 | method that we saw in the previous chapter of the tutorial, there is an |
55490817 |
230 | alternate approach that allows us to be more specific while also |
231 | paving the way for more advanced capabilities. Change the method |
232 | declaration for C<url_create> in C<lib/MyApp/Controller/Books.pm> you |
89d3dae9 |
233 | entered above to match the following: |
234 | |
235 | sub url_create :Chained('/') :PathPart('books/url_create') :Args(3) { |
236 | |
55490817 |
237 | This converts the method to take advantage of the Chained |
72609296 |
238 | action/dispatch type. Chaining lets you have a single URL |
55490817 |
239 | automatically dispatch to several controller methods, each of which |
240 | can have precise control over the number of arguments that it will |
89d3dae9 |
241 | receive. A chain can essentially be thought of having three parts -- |
72609296 |
242 | a beginning, a middle, and an end. The bullets below summarize the key |
89d3dae9 |
243 | points behind each of these parts of a chain: |
244 | |
245 | |
246 | =over 4 |
247 | |
248 | |
249 | =item * |
250 | |
251 | Beginning |
252 | |
253 | =over 4 |
254 | |
255 | =item * |
256 | |
257 | B<Use "C<:Chained('/')>" to start a chain> |
258 | |
259 | =item * |
260 | |
261 | Get arguments through C<CaptureArgs()> |
262 | |
263 | =item * |
264 | |
265 | Specify the path to match with C<PathPart()> |
266 | |
267 | =back |
268 | |
269 | |
270 | =item * |
271 | |
272 | Middle |
273 | |
274 | =over 4 |
275 | |
276 | =item * |
d442cc9f |
277 | |
89d3dae9 |
278 | Link to previous part of the chain with C<:Chained('_name_')> |
279 | |
280 | =item * |
281 | |
282 | Get arguments through C<CaptureArgs()> |
283 | |
284 | =item * |
285 | |
286 | Specify the path to match with C<PathPart()> |
287 | |
288 | =back |
289 | |
290 | |
291 | =item * |
292 | |
293 | End |
294 | |
295 | =over 4 |
296 | |
297 | =item * |
298 | |
299 | Link to previous part of the chain with C<:Chained('_name_')> |
300 | |
301 | =item * |
302 | |
303 | B<Do NOT get arguments through "C<CaptureArgs()>," use "C<Args()>" instead to end a chain> |
304 | |
305 | =item * |
306 | |
307 | Specify the path to match with C<PathPart()> |
308 | |
309 | =back |
310 | |
311 | |
312 | =back |
313 | |
72609296 |
314 | In our C<url_create> method above, we have combined all three parts into |
315 | a single method: C<:Chained('/')> to start the chain, |
316 | C<:PathPart('books/url_create')> to specify the base URL to match, and |
317 | C<:Args(3)> to capture exactly three arguments and to end the chain. |
89d3dae9 |
318 | |
55490817 |
319 | As we will see shortly, a chain can consist of as many "links" as you |
320 | wish, with each part capturing some arguments and doing some work |
321 | along the way. We will continue to use the Chained action type in this |
4b4d3884 |
322 | chapter of the tutorial and explore slightly more advanced capabilities |
55490817 |
323 | with the base method and delete feature below. But Chained dispatch |
324 | is capable of far more. For additional information, see |
325 | L<Catalyst::Manual::Intro/Action types>, |
326 | L<Catalyst::DispatchType::Chained|Catalyst::DispatchType::Chained>, |
72609296 |
327 | and the 2006 Advent calendar entry on the subject: |
89d3dae9 |
328 | L<http://www.catalystframework.org/calendar/2006/10>. |
329 | |
330 | |
331 | =head2 Try the Chained Action |
332 | |
55490817 |
333 | If you look back at the development server startup logs from your |
334 | initial version of the C<url_create> method (the one using the |
89d3dae9 |
335 | C<:Local> attribute), you will notice that it produced output similar |
336 | to the following: |
337 | |
fbbb9084 |
338 | [debug] Loaded Path actions: |
339 | .-------------------------------------+--------------------------------------. |
340 | | Path | Private | |
341 | +-------------------------------------+--------------------------------------+ |
342 | | / | /default | |
343 | | / | /index | |
344 | | /books | /books/index | |
345 | | /books/list | /books/list | |
346 | | /books/url_create | /books/url_create | |
347 | '-------------------------------------+--------------------------------------' |
89d3dae9 |
348 | |
55490817 |
349 | Now start the development server with our basic chained method in |
350 | place and the startup debug output should change to something along |
89d3dae9 |
351 | the lines of the following: |
352 | |
fbbb9084 |
353 | [debug] Loaded Path actions: |
354 | .-------------------------------------+--------------------------------------. |
355 | | Path | Private | |
356 | +-------------------------------------+--------------------------------------+ |
357 | | / | /default | |
358 | | / | /index | |
359 | | /books | /books/index | |
360 | | /books/list | /books/list | |
361 | '-------------------------------------+--------------------------------------' |
55490817 |
362 | |
fbbb9084 |
363 | [debug] Loaded Chained actions: |
364 | .-------------------------------------+--------------------------------------. |
365 | | Path Spec | Private | |
366 | +-------------------------------------+--------------------------------------+ |
367 | | /books/url_create/*/*/* | /books/url_create | |
368 | '-------------------------------------+--------------------------------------' |
89d3dae9 |
369 | |
55490817 |
370 | C<url_create> has disappeared form the "Loaded Path actions" section |
371 | but it now shows up under the newly created "Loaded Chained actions" |
72609296 |
372 | section. And the "/*/*/*" portion clearly shows our requirement for |
fbbb9084 |
373 | three arguments. |
89d3dae9 |
374 | |
55490817 |
375 | As with our non-chained version of C<url_create>, use your browser to |
89d3dae9 |
376 | enter the following URL: |
377 | |
fbbb9084 |
378 | http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4 |
89d3dae9 |
379 | |
55490817 |
380 | You should see the same "Added book 'TCPIP_Illustrated_Vol-2' by |
381 | 'Stevens' with a rating of 5." along with a dump of the new book model |
72609296 |
382 | object. Click the "Return to list" link, and you should find that there |
383 | are now seven books shown (two copies of I<TCPIP_Illustrated_Vol-2>). |
89d3dae9 |
384 | |
385 | |
8a472b34 |
386 | =head2 Refactor to Use a 'base' Method to Start the Chains |
89d3dae9 |
387 | |
55490817 |
388 | Let's make a quick update to our initial Chained action to show a |
389 | little more of the power of chaining. First, open |
89d3dae9 |
390 | C<lib/MyApp/Controller/Books.pm> in your editor and add the following |
391 | method: |
392 | |
fbbb9084 |
393 | =head2 base |
55490817 |
394 | |
fbbb9084 |
395 | Can place common logic to start chained dispatch here |
55490817 |
396 | |
fbbb9084 |
397 | =cut |
55490817 |
398 | |
fbbb9084 |
399 | sub base :Chained('/') :PathPart('books') :CaptureArgs(0) { |
400 | my ($self, $c) = @_; |
55490817 |
401 | |
1cde0fd6 |
402 | # Store the ResultSet in stash so it's available for other methods |
fbbb9084 |
403 | $c->stash->{resultset} = $c->model('DB::Books'); |
55490817 |
404 | |
fbbb9084 |
405 | # Print a message to the debug log |
406 | $c->log->debug('*** INSIDE BASE METHOD ***'); |
407 | } |
408 | |
55490817 |
409 | Here we print a log message and store the DBIC ResultSet in |
410 | C<$c-E<gt>stash-E<gt>{resultset}> so that it's automatically available |
411 | for other actions that chain off C<base>. If your controller always |
72609296 |
412 | needs a book ID as its first argument, you could have the base method |
55490817 |
413 | capture that argument (with C<:CaptureArgs(1)>) and use it to pull the |
414 | book object with C<-E<gt>find($id)> and leave it in the stash for |
415 | later parts of your chains to then act upon. Because we have several |
994b66ad |
416 | actions that don't need to retrieve a book (such as the C<url_create> |
417 | we are working with now), we will instead add that functionality |
418 | to a common C<object> action shortly. |
419 | |
55490817 |
420 | As for C<url_create>, let's modify it to first dispatch to C<base>. |
421 | Open up C<lib/MyApp/Controller/Books.pm> and edit the declaration for |
994b66ad |
422 | C<url_create> to match the following: |
89d3dae9 |
423 | |
424 | sub url_create :Chained('base') :PathPart('url_create') :Args(3) { |
425 | |
55490817 |
426 | Next, try out the refactored chain by restarting the development |
427 | server. Notice that our "Loaded Chained actions" section has changed |
fbbb9084 |
428 | slightly: |
55490817 |
429 | |
fbbb9084 |
430 | [debug] Loaded Chained actions: |
431 | .-------------------------------------+--------------------------------------. |
432 | | Path Spec | Private | |
433 | +-------------------------------------+--------------------------------------+ |
434 | | /books/url_create/*/*/* | /books/base (0) | |
435 | | | => /books/url_create | |
436 | '-------------------------------------+--------------------------------------' |
89d3dae9 |
437 | |
55490817 |
438 | The "Path Spec" is the same, but now it maps to two Private actions as |
89d3dae9 |
439 | we would expect. |
440 | |
441 | Once again, enter the following URL into your browser: |
442 | |
fbbb9084 |
443 | http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4 |
89d3dae9 |
444 | |
55490817 |
445 | The same "Added book 'TCPIP_Illustrated_Vol-2' by 'Stevens' with a |
72609296 |
446 | rating of 5." message and a dump of the new book object should appear. |
55490817 |
447 | Also notice the extra debug message in the development server output |
72609296 |
448 | from the C<base> method. Click the "Return to list" link, and you |
449 | should find that there are now eight books shown. |
d442cc9f |
450 | |
451 | |
452 | =head1 MANUALLY BUILDING A CREATE FORM |
453 | |
454 | Although the C<url_create> action in the previous step does begin to |
455 | reveal the power and flexibility of both Catalyst and DBIC, it's |
456 | obviously not a very realistic example of how users should be expected |
457 | to enter data. This section begins to address that concern. |
458 | |
459 | |
460 | =head2 Add Method to Display The Form |
461 | |
462 | Edit C<lib/MyApp/Controller/Books.pm> and add the following method: |
463 | |
464 | =head2 form_create |
55490817 |
465 | |
d442cc9f |
466 | Display form to collect information for book to create |
55490817 |
467 | |
d442cc9f |
468 | =cut |
55490817 |
469 | |
89d3dae9 |
470 | sub form_create :Chained('base') :PathPart('form_create') :Args(0) { |
d442cc9f |
471 | my ($self, $c) = @_; |
55490817 |
472 | |
d442cc9f |
473 | # Set the TT template to use |
474 | $c->stash->{template} = 'books/form_create.tt2'; |
475 | } |
476 | |
72609296 |
477 | This action simply invokes a view containing a form to create a book. |
d442cc9f |
478 | |
1390ef0e |
479 | |
d442cc9f |
480 | =head2 Add a Template for the Form |
481 | |
482 | Open C<root/src/books/form_create.tt2> in your editor and enter: |
483 | |
484 | [% META title = 'Manual Form Book Create' -%] |
55490817 |
485 | |
8a7c5151 |
486 | <form method="post" action="[% c.uri_for('form_create_do') %]"> |
d442cc9f |
487 | <table> |
488 | <tr><td>Title:</td><td><input type="text" name="title"></td></tr> |
489 | <tr><td>Rating:</td><td><input type="text" name="rating"></td></tr> |
490 | <tr><td>Author ID:</td><td><input type="text" name="author_id"></td></tr> |
491 | </table> |
492 | <input type="submit" name="Submit" value="Submit"> |
493 | </form> |
494 | |
495 | Note that we have specified the target of the form data as |
496 | C<form_create_do>, the method created in the section that follows. |
497 | |
1390ef0e |
498 | |
d442cc9f |
499 | =head2 Add a Method to Process Form Values and Update Database |
500 | |
501 | Edit C<lib/MyApp/Controller/Books.pm> and add the following method to |
502 | save the form information to the database: |
503 | |
504 | =head2 form_create_do |
55490817 |
505 | |
d442cc9f |
506 | Take information from form and add to database |
55490817 |
507 | |
d442cc9f |
508 | =cut |
55490817 |
509 | |
89d3dae9 |
510 | sub form_create_do :Chained('base') :PathPart('form_create_do') :Args(0) { |
d442cc9f |
511 | my ($self, $c) = @_; |
55490817 |
512 | |
d442cc9f |
513 | # Retrieve the values from the form |
514 | my $title = $c->request->params->{title} || 'N/A'; |
515 | my $rating = $c->request->params->{rating} || 'N/A'; |
516 | my $author_id = $c->request->params->{author_id} || '1'; |
55490817 |
517 | |
d442cc9f |
518 | # Create the book |
d0496197 |
519 | my $book = $c->model('DB::Books')->create({ |
d442cc9f |
520 | title => $title, |
521 | rating => $rating, |
522 | }); |
523 | # Handle relationship with author |
524 | $book->add_to_book_authors({author_id => $author_id}); |
55490817 |
525 | |
d442cc9f |
526 | # Store new model object in stash |
527 | $c->stash->{book} = $book; |
55490817 |
528 | |
d442cc9f |
529 | # Avoid Data::Dumper issue mentioned earlier |
55490817 |
530 | # You can probably omit this |
d442cc9f |
531 | $Data::Dumper::Useperl = 1; |
55490817 |
532 | |
d442cc9f |
533 | # Set the TT template to use |
534 | $c->stash->{template} = 'books/create_done.tt2'; |
535 | } |
536 | |
537 | |
538 | =head2 Test Out The Form |
539 | |
540 | If the application is still running from before, use C<Ctrl-C> to kill |
541 | it. Then restart the server: |
542 | |
543 | $ script/myapp_server.pl |
544 | |
55490817 |
545 | Notice that the server startup log reflects the two new chained |
89d3dae9 |
546 | methods that we added: |
547 | |
fbbb9084 |
548 | [debug] Loaded Chained actions: |
549 | .-------------------------------------+--------------------------------------. |
550 | | Path Spec | Private | |
551 | +-------------------------------------+--------------------------------------+ |
552 | | /books/form_create | /books/base (0) | |
553 | | | => /books/form_create | |
554 | | /books/form_create_do | /books/base (0) | |
555 | | | => /books/form_create_do | |
556 | | /books/url_create/*/*/* | /books/base (0) | |
557 | | | => /books/url_create | |
558 | '-------------------------------------+--------------------------------------' |
89d3dae9 |
559 | |
d442cc9f |
560 | Point your browser to L<http://localhost:3000/books/form_create> and |
561 | enter "TCP/IP Illustrated, Vol 3" for the title, a rating of 5, and an |
1390ef0e |
562 | author ID of 4. You should then see the output of the same |
d442cc9f |
563 | C<create_done.tt2> template seen in earlier examples. Finally, click |
564 | "Return to list" to view the full list of books. |
565 | |
566 | B<Note:> Having the user enter the primary key ID for the author is |
567 | obviously crude; we will address this concern with a drop-down list in |
4b4d3884 |
568 | Chapter 9. |
d442cc9f |
569 | |
570 | |
571 | =head1 A SIMPLE DELETE FEATURE |
572 | |
72609296 |
573 | Turning our attention to the Delete portion of CRUD, this section |
d442cc9f |
574 | illustrates some basic techniques that can be used to remove information |
575 | from the database. |
576 | |
577 | |
578 | =head2 Include a Delete Link in the List |
579 | |
1cde0fd6 |
580 | Edit C<root/src/books/list.tt2> and update it to match the following (two |
d442cc9f |
581 | sections have changed: 1) the additional '<th>Links</th>' table header, |
72609296 |
582 | and 2) the four lines for the Delete link near the bottom): |
d442cc9f |
583 | |
584 | [% # This is a TT comment. The '-' at the end "chomps" the newline. You won't -%] |
585 | [% # see this "chomping" in your browser because HTML ignores blank lines, but -%] |
586 | [% # it WILL eliminate a blank line if you view the HTML source. It's purely -%] |
587 | [%- # optional, but both the beginning and the ending TT tags support chomping. -%] |
55490817 |
588 | |
d442cc9f |
589 | [% # Provide a title to root/lib/site/header -%] |
590 | [% META title = 'Book List' -%] |
55490817 |
591 | |
d442cc9f |
592 | <table> |
593 | <tr><th>Title</th><th>Rating</th><th>Author(s)</th><th>Links</th></tr> |
594 | [% # Display each book in a table row %] |
595 | [% FOREACH book IN books -%] |
596 | <tr> |
597 | <td>[% book.title %]</td> |
598 | <td>[% book.rating %]</td> |
599 | <td> |
600 | [% # First initialize a TT variable to hold a list. Then use a TT FOREACH -%] |
601 | [% # loop in 'side effect notation' to load just the last names of the -%] |
55490817 |
602 | [% # authors into the list. Note that the 'push' TT vmethod doesn't return -%] |
d442cc9f |
603 | [% # a value, so nothing will be printed here. But, if you have something -%] |
55490817 |
604 | [% # in TT that does return a value and you don't want it printed, you can -%] |
605 | [% # 1) assign it to a bogus value, or -%] |
606 | [% # 2) use the CALL keyword to call it and discard the return value. -%] |
d442cc9f |
607 | [% tt_authors = [ ]; |
608 | tt_authors.push(author.last_name) FOREACH author = book.authors %] |
609 | [% # Now use a TT 'virtual method' to display the author count in parens -%] |
610 | ([% tt_authors.size %]) |
611 | [% # Use another TT vmethod to join & print the names & comma separators -%] |
612 | [% tt_authors.join(', ') %] |
613 | </td> |
614 | <td> |
615 | [% # Add a link to delete a book %] |
e075db0c |
616 | <a href="[% c.uri_for(c.controller.action_for('delete'), [book.id]) %]">Delete</a> |
d442cc9f |
617 | </td> |
618 | </tr> |
619 | [% END -%] |
620 | </table> |
621 | |
55490817 |
622 | The additional code is obviously designed to add a new column to the |
72609296 |
623 | right side of the table with a C<Delete> "button" (for simplicity, links |
624 | will be used instead of full HTML buttons; in practice, anything that |
625 | modifies data should be handled with a form sending a PUT request). |
fe01b24f |
626 | |
55490817 |
627 | Also notice that we are using a more advanced form of C<uri_for> than |
628 | we have seen before. Here we use |
629 | C<$c-E<gt>controller-E<gt>action_for> to automatically generate a URI |
630 | appropriate for that action based on the method we want to link to |
631 | while inserting the C<book.id> value into the appropriate place. Now, |
632 | if you ever change C<:PathPart('delete')> in your controller method to |
633 | C<:PathPart('kill')>, then your links will automatically update |
634 | without any changes to your .tt2 template file. As long as the name |
72609296 |
635 | of your method does not change (here, "delete"), then your links will |
55490817 |
636 | still be correct. There are a few shortcuts and options when using |
0416017e |
637 | C<action_for()>: |
638 | |
639 | =over 4 |
640 | |
641 | =item * |
642 | |
643 | If you are referring to a method in the current controller, you can |
644 | use C<$self-E<gt>action_for('_method_name_')>. |
645 | |
646 | =item * |
647 | |
648 | If you are referring to a method in a different controller, you need |
fbbb9084 |
649 | to include that controller's name as an argument to C<controller()>, as in |
0416017e |
650 | C<$c-E<gt>controller('_controller_name_')-E<gt>action_for('_method_name_')>. |
651 | |
652 | =back |
b2ad8bbd |
653 | |
55490817 |
654 | B<Note:> In practice you should B<never> use a GET request to delete a |
655 | record -- always use POST for actions that will modify data. We are |
c5d94181 |
656 | doing it here for illustrative and simplicity purposes only. |
d442cc9f |
657 | |
1390ef0e |
658 | |
994b66ad |
659 | =head2 Add a Common Method to Retrieve a Book for the Chain |
660 | |
55490817 |
661 | As mentioned earlier, since we have a mixture of actions that operate |
662 | on a single book ID and others that do not, we should not have C<base> |
663 | capture the book ID, find the corresponding book in the database and |
664 | save it in the stash for later links in the chain. However, just |
665 | because that logic does not belong in C<base> doesn't mean that we |
666 | can't create another location to centralize the book lookup code. In |
667 | our case, we will create a method called C<object> that will store the |
668 | specific book in the stash. Chains that always operate on a single |
669 | existing book can chain off this method, but methods such as |
670 | C<url_create> that don't operate on an existing book can chain |
fbbb9084 |
671 | directly off base. |
994b66ad |
672 | |
673 | To add the C<object> method, edit C<lib/MyApp/Controller/Books.pm> |
674 | and add the following code: |
675 | |
e075db0c |
676 | =head2 object |
55490817 |
677 | |
e075db0c |
678 | Fetch the specified book object based on the book ID and store |
679 | it in the stash |
55490817 |
680 | |
e075db0c |
681 | =cut |
55490817 |
682 | |
994b66ad |
683 | sub object :Chained('base') :PathPart('id') :CaptureArgs(1) { |
fbbb9084 |
684 | # $id = primary key of book to delete |
994b66ad |
685 | my ($self, $c, $id) = @_; |
55490817 |
686 | |
994b66ad |
687 | # Find the book object and store it in the stash |
688 | $c->stash(object => $c->stash->{resultset}->find($id)); |
55490817 |
689 | |
994b66ad |
690 | # Make sure the lookup was successful. You would probably |
691 | # want to do something like this in a real app: |
692 | # $c->detach('/error_404') if !$c->stash->{object}; |
693 | die "Book $id not found!" if !$c->stash->{object}; |
694 | } |
695 | |
696 | Now, any other method that chains off C<object> will automatically |
55490817 |
697 | have the appropriate book waiting for it in |
acbd7bdd |
698 | C<$c-E<gt>stash-E<gt>{object}>. |
994b66ad |
699 | |
72609296 |
700 | Also note that we are using a different technique for setting |
701 | C<$c-E<gt>stash>. The advantage of this style is that it lets you set |
702 | multiple stash variables at a time. For example: |
994b66ad |
703 | |
704 | $c->stash(object => $c->stash->{resultset}->find($id), |
705 | another_thing => 1); |
706 | |
707 | or as a hashref: |
708 | |
709 | $c->stash({object => $c->stash->{resultset}->find($id), |
710 | another_thing => 1}); |
711 | |
fbbb9084 |
712 | Either format works, but the C<$c-E<gt>stash(name =E<gt> value);> |
72609296 |
713 | style is growing in popularity -- you may wish to use it all |
994b66ad |
714 | the time (even when you are only setting a single value). |
715 | |
716 | |
d442cc9f |
717 | =head2 Add a Delete Action to the Controller |
718 | |
719 | Open C<lib/MyApp/Controller/Books.pm> in your editor and add the |
720 | following method: |
721 | |
1390ef0e |
722 | =head2 delete |
55490817 |
723 | |
d442cc9f |
724 | Delete a book |
55490817 |
725 | |
d442cc9f |
726 | =cut |
55490817 |
727 | |
994b66ad |
728 | sub delete :Chained('object') :PathPart('delete') :Args(0) { |
994b66ad |
729 | my ($self, $c) = @_; |
55490817 |
730 | |
994b66ad |
731 | # Use the book object saved by 'object' and delete it along |
732 | # with related 'book_authors' entries |
733 | $c->stash->{object}->delete; |
55490817 |
734 | |
d442cc9f |
735 | # Set a status message to be displayed at the top of the view |
736 | $c->stash->{status_msg} = "Book deleted."; |
55490817 |
737 | |
d442cc9f |
738 | # Forward to the list action/method in this controller |
739 | $c->forward('list'); |
740 | } |
741 | |
55490817 |
742 | This method first deletes the book object saved by the C<object> method. |
743 | However, it also removes the corresponding entry from the |
fbbb9084 |
744 | C<book_authors> table with a cascading delete. |
d442cc9f |
745 | |
746 | Then, rather than forwarding to a "delete done" page as we did with the |
747 | earlier create example, it simply sets the C<status_msg> to display a |
748 | notification to the user as the normal list view is rendered. |
749 | |
750 | The C<delete> action uses the context C<forward> method to return the |
751 | user to the book list. The C<detach> method could have also been used. |
752 | Whereas C<forward> I<returns> to the original action once it is |
753 | completed, C<detach> does I<not> return. Other than that, the two are |
754 | equivalent. |
755 | |
756 | |
757 | =head2 Try the Delete Feature |
758 | |
759 | If the application is still running from before, use C<Ctrl-C> to kill |
760 | it. Then restart the server: |
761 | |
994b66ad |
762 | $ DBIC_TRACE=1 script/myapp_server.pl |
d442cc9f |
763 | |
89d3dae9 |
764 | The C<delete> method now appears in the "Loaded Chained actions" section |
765 | of the startup debug output: |
766 | |
fbbb9084 |
767 | [debug] Loaded Chained actions: |
994b66ad |
768 | .-------------------------------------+--------------------------------------. |
769 | | Path Spec | Private | |
770 | +-------------------------------------+--------------------------------------+ |
771 | | /books/id/*/delete | /books/base (0) | |
772 | | | -> /books/object (1) | |
773 | | | => /books/delete | |
774 | | /books/form_create | /books/base (0) | |
775 | | | => /books/form_create | |
776 | | /books/form_create_do | /books/base (0) | |
777 | | | => /books/form_create_do | |
778 | | /books/url_create/*/*/* | /books/base (0) | |
779 | | | => /books/url_create | |
780 | '-------------------------------------+--------------------------------------' |
89d3dae9 |
781 | |
d442cc9f |
782 | Then point your browser to L<http://localhost:3000/books/list> and click |
55490817 |
783 | the "Delete" link next to the first "TCPIP_Illustrated_Vol-2". A green |
784 | "Book deleted" status message should display at the top of the page, |
994b66ad |
785 | along with a list of the eight remaining books. You will also see the |
786 | cascading delete operation via the DBIC_TRACE output: |
787 | |
acbd7bdd |
788 | SELECT me.id, me.title, me.rating FROM books me WHERE ( ( me.id = ? ) ): '6' |
994b66ad |
789 | DELETE FROM books WHERE ( id = ? ): '6' |
790 | SELECT me.book_id, me.author_id FROM book_authors me WHERE ( me.book_id = ? ): '6' |
791 | DELETE FROM book_authors WHERE ( author_id = ? AND book_id = ? ): '4', '6' |
d442cc9f |
792 | |
793 | |
794 | =head2 Fixing a Dangerous URL |
795 | |
55490817 |
796 | Note the URL in your browser once you have performed the deletion in the |
d442cc9f |
797 | prior step -- it is still referencing the delete action: |
798 | |
acbd7bdd |
799 | http://localhost:3000/books/id/6/delete |
d442cc9f |
800 | |
55490817 |
801 | What if the user were to press reload with this URL still active? In |
802 | this case the redundant delete is harmless (although it does generate |
803 | an exception screen, it doesn't perform any undesirable actions on the |
804 | application or database), but in other cases this could clearly be |
fbbb9084 |
805 | extremely dangerous. |
d442cc9f |
806 | |
807 | We can improve the logic by converting to a redirect. Unlike |
808 | C<$c-E<gt>forward('list'))> or C<$c-E<gt>detach('list'))> that perform |
809 | a server-side alteration in the flow of processing, a redirect is a |
3533daff |
810 | client-side mechanism that causes the browser to issue an entirely |
d442cc9f |
811 | new request. As a result, the URL in the browser is updated to match |
812 | the destination of the redirection URL. |
813 | |
814 | To convert the forward used in the previous section to a redirect, |
55490817 |
815 | open C<lib/MyApp/Controller/Books.pm> and edit the existing |
d442cc9f |
816 | C<sub delete> method to match: |
817 | |
994b66ad |
818 | =head2 delete |
55490817 |
819 | |
d442cc9f |
820 | Delete a book |
55490817 |
821 | |
d442cc9f |
822 | =cut |
55490817 |
823 | |
994b66ad |
824 | sub delete :Chained('object') :PathPart('delete') :Args(0) { |
fbbb9084 |
825 | my ($self, $c) = @_; |
55490817 |
826 | |
994b66ad |
827 | # Use the book object saved by 'object' and delete it along |
828 | # with related 'book_authors' entries |
829 | $c->stash->{object}->delete; |
55490817 |
830 | |
d442cc9f |
831 | # Set a status message to be displayed at the top of the view |
832 | $c->stash->{status_msg} = "Book deleted."; |
55490817 |
833 | |
0416017e |
834 | # Redirect the user back to the list page. Note the use |
835 | # of $self->action_for as earlier in this section (BasicCRUD) |
fbbb9084 |
836 | $c->response->redirect($c->uri_for($self->action_for('list'))); |
d442cc9f |
837 | } |
838 | |
839 | |
840 | =head2 Try the Delete and Redirect Logic |
841 | |
55490817 |
842 | Restart the development server and point your browser to |
843 | L<http://localhost:3000/books/list> (don't just hit "Refresh" in your |
844 | browser since we left the URL in an invalid state in the previous |
845 | section!) and delete the first copy of the remaining two |
846 | "TCPIP_Illustrated_Vol-2" books. The URL in your browser should return |
847 | to the L<http://localhost:3000/books/list> URL, so that is an |
848 | improvement, but notice that I<no green "Book deleted" status message is |
849 | displayed>. Because the stash is reset on every request (and a redirect |
850 | involves a second request), the C<status_msg> is cleared before it can |
994b66ad |
851 | be displayed. |
d442cc9f |
852 | |
853 | |
8a472b34 |
854 | =head2 Using 'uri_for' to Pass Query Parameters |
d442cc9f |
855 | |
4b4d3884 |
856 | There are several ways to pass information across a redirect. One |
857 | option is to use the C<flash> technique that we will see in Chapter 5 |
72609296 |
858 | of this tutorial; however, here we will pass the information via query |
4b4d3884 |
859 | parameters on the redirect itself. Open |
860 | C<lib/MyApp/Controller/Books.pm> and update the existing C<sub delete> |
89d3dae9 |
861 | method to match the following: |
d442cc9f |
862 | |
55490817 |
863 | =head2 delete |
864 | |
d442cc9f |
865 | Delete a book |
55490817 |
866 | |
d442cc9f |
867 | =cut |
55490817 |
868 | |
994b66ad |
869 | sub delete :Chained('object') :PathPart('delete') :Args(0) { |
fbbb9084 |
870 | my ($self, $c) = @_; |
55490817 |
871 | |
994b66ad |
872 | # Use the book object saved by 'object' and delete it along |
873 | # with related 'book_authors' entries |
874 | $c->stash->{object}->delete; |
55490817 |
875 | |
d442cc9f |
876 | # Redirect the user back to the list page with status msg as an arg |
55490817 |
877 | $c->response->redirect($c->uri_for($self->action_for('list'), |
d442cc9f |
878 | {status_msg => "Book deleted."})); |
879 | } |
880 | |
881 | This modification simply leverages the ability of C<uri_for> to include |
55490817 |
882 | an arbitrary number of name/value pairs in a hash reference. Next, we |
883 | need to update C<root/src/wrapper.tt2> to handle C<status_msg> as a |
d442cc9f |
884 | query parameter: |
885 | |
1390ef0e |
886 | ... |
d442cc9f |
887 | <div id="content"> |
1390ef0e |
888 | [%# Status and error messages %] |
889 | <span class="message">[% status_msg || c.request.params.status_msg %]</span> |
890 | <span class="error">[% error_msg %]</span> |
891 | [%# This is where TT will stick all of your template's contents. -%] |
892 | [% content %] |
893 | </div><!-- end content --> |
894 | ... |
895 | |
55490817 |
896 | Although the sample above only shows the C<content> div, leave the |
1390ef0e |
897 | rest of the file intact -- the only change we made to the C<wrapper.tt2> |
55490817 |
898 | was to add "C<|| c.request.params.status_msg>" to the |
1390ef0e |
899 | C<E<lt>span class="message"E<gt>> line. |
d442cc9f |
900 | |
901 | |
902 | =head2 Try the Delete and Redirect With Query Param Logic |
903 | |
55490817 |
904 | Restart the development server and point your browser to |
905 | L<http://localhost:3000/books/list> (you should now be able to safely |
906 | hit "refresh" in your browser). Then delete the remaining copy of |
907 | "TCPIP_Illustrated_Vol-2". The green "Book deleted" status message |
d442cc9f |
908 | should return. |
909 | |
55490817 |
910 | B<NOTE:> Another popular method for maintaining server-side |
911 | information across a redirect is to use the C<flash> technique we |
4b4d3884 |
912 | discuss in the next chapter of the tutorial, |
55490817 |
913 | L<Authentication|Catalyst::Manual::Tutorial::Authentication>. While |
914 | C<flash> is a "slicker" mechanism in that it's all handled by the |
915 | server and doesn't "pollute" your URLs, B<it is important to note that |
916 | C<flash> can lead to situations where the wrong information shows up |
917 | in the wrong browser window if the user has multiple windows or |
72609296 |
918 | browser tabs open>. For example, Window A causes something to be |
55490817 |
919 | placed in the stash, but before that window performs a redirect, |
920 | Window B makes a request to the server and gets the status information |
994b66ad |
921 | that should really go to Window A. For this reason, you may wish |
89d3dae9 |
922 | to use the "query param" technique shown here in your applications. |
d442cc9f |
923 | |
924 | |
1cde0fd6 |
925 | =head1 EXPLORING THE POWER OF DBIC |
926 | |
55490817 |
927 | In this section we will explore some additional capabilities offered |
928 | by DBIx::Class. Although these features have relatively little to do |
929 | with Catalyst per se, you will almost certainly want to take advantage |
1cde0fd6 |
930 | of them in your applications. |
931 | |
932 | |
1cde0fd6 |
933 | =head2 Add Datetime Columns to Our Existing Books Table |
934 | |
55490817 |
935 | Let's add two columns to our existing C<books> table to track when |
1cde0fd6 |
936 | each book was added and when each book is updated: |
937 | |
938 | $ sqlite3 myapp.db |
939 | sqlite> ALTER TABLE books ADD created INTEGER; |
940 | sqlite> ALTER TABLE books ADD updated INTEGER; |
941 | sqlite> UPDATE books SET created = DATETIME('NOW'), updated = DATETIME('NOW'); |
942 | sqlite> SELECT * FROM books; |
acbd7bdd |
943 | 1|CCSP SNRS Exam Certification Guide|5|2009-03-08 16:26:35|2009-03-08 16:26:35 |
944 | 2|TCP/IP Illustrated, Volume 1|5|2009-03-08 16:26:35|2009-03-08 16:26:35 |
945 | 3|Internetworking with TCP/IP Vol.1|4|2009-03-08 16:26:35|2009-03-08 16:26:35 |
946 | 4|Perl Cookbook|5|2009-03-08 16:26:35|2009-03-08 16:26:35 |
947 | 5|Designing with Web Standards|5|2009-03-08 16:26:35|2009-03-08 16:26:35 |
948 | 9|TCP/IP Illustrated, Vol 3|5|2009-03-08 16:26:35|2009-03-08 16:26:35 |
1cde0fd6 |
949 | sqlite> .quit |
950 | $ |
951 | |
952 | This will modify the C<books> table to include the two new fields |
953 | and populate those fields with the current time. |
954 | |
acbd7bdd |
955 | |
1cde0fd6 |
956 | =head2 Update DBIC to Automatically Handle the Datetime Columns |
957 | |
958 | Next, we should re-run the DBIC helper to update the Result Classes |
959 | with the new fields: |
960 | |
961 | $ script/myapp_create.pl model DB DBIC::Schema MyApp::Schema \ |
962 | create=static components=TimeStamp dbi:SQLite:myapp.db |
963 | exists "/root/dev/MyApp/script/../lib/MyApp/Model" |
964 | exists "/root/dev/MyApp/script/../t" |
965 | Dumping manual schema for MyApp::Schema to directory /root/dev/MyApp/script/../lib ... |
966 | Schema dump completed. |
967 | exists "/root/dev/MyApp/script/../lib/MyApp/Model/DB.pm" |
968 | |
969 | Notice that we modified our use of the helper slightly: we told |
d05dbc42 |
970 | it to include the L<DBIx::Class::TimeStamp|DBIx::Class::TimeStamp> |
1cde0fd6 |
971 | in the C<load_components> line of the Result Classes. |
972 | |
55490817 |
973 | If you open C<lib/MyApp/Schema/Result/Books.pm> in your editor you |
974 | should see that the C<created> and C<updated> fields are now included |
72609296 |
975 | in the call to C<add_columns()>, but our relationship information below |
55490817 |
976 | the "C<# DO NOT MODIFY...>" line was automatically preserved. |
1cde0fd6 |
977 | |
55490817 |
978 | While we have this file open, let's update it with some additional |
979 | information to have DBIC automatically handle the updating of these |
980 | two fields for us. Insert the following code at the bottom of the |
981 | file (it B<must> be B<below> the "C<# DO NOT MODIFY...>" line and |
1cde0fd6 |
982 | B<above> the C<1;> on the last line): |
983 | |
984 | # |
985 | # Enable automatic date handling |
986 | # |
987 | __PACKAGE__->add_columns( |
988 | "created", |
989 | { data_type => 'datetime', set_on_create => 1 }, |
990 | "updated", |
991 | { data_type => 'datetime', set_on_create => 1, set_on_update => 1 }, |
55490817 |
992 | ); |
1cde0fd6 |
993 | |
55490817 |
994 | This will override the definition for these fields that Schema::Loader |
995 | placed at the top of the file. The C<set_on_create> and |
996 | C<set_on_update> options will cause DBIC to automatically update the |
1cde0fd6 |
997 | timestamps in these columns whenever a row is created or modified. |
998 | |
999 | To test this out, restart the development server using the |
1000 | C<DBIC_TRACE=1> option: |
1001 | |
1002 | DBIC_TRACE=1 script/myapp_server.pl |
1003 | |
1004 | Then enter the following URL into your web browser: |
1005 | |
1006 | http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4 |
1007 | |
1008 | You should get the same "Book Created" screen we saw above. However, |
1009 | if you now use the sqlite3 command-line tool to dump the C<books> table, |
1010 | you will see that the new book we added has an appropriate date and |
1011 | time entered for it (see the last line in the listing below): |
1012 | |
1013 | sqlite3 myapp.db "select * from books" |
acbd7bdd |
1014 | 1|CCSP SNRS Exam Certification Guide|5|2009-03-08 16:26:35|2009-03-08 16:26:35 |
1015 | 2|TCP/IP Illustrated, Volume 1|5|2009-03-08 16:26:35|2009-03-08 16:26:35 |
1016 | 3|Internetworking with TCP/IP Vol.1|4|2009-03-08 16:26:35|2009-03-08 16:26:35 |
1017 | 4|Perl Cookbook|5|2009-03-08 16:26:35|2009-03-08 16:26:35 |
1018 | 5|Designing with Web Standards|5|2009-03-08 16:26:35|2009-03-08 16:26:35 |
1019 | 9|TCP/IP Illustrated, Vol 3|5|2009-03-08 16:26:35|2009-03-08 16:26:35 |
1020 | 10|TCPIP_Illustrated_Vol-2|5|2009-03-08 16:29:08|2009-03-08 16:29:08 |
1cde0fd6 |
1021 | |
55490817 |
1022 | Notice in the debug log that the SQL DBIC generated has changed to |
1cde0fd6 |
1023 | incorporate the datetime logic: |
1024 | |
55490817 |
1025 | INSERT INTO books (created, rating, title, updated) VALUES (?, ?, ?, ?): |
acbd7bdd |
1026 | '2009-03-08 16:29:08', '5', 'TCPIP_Illustrated_Vol-2', '2009-03-08 16:29:08' |
1cde0fd6 |
1027 | INSERT INTO book_authors (author_id, book_id) VALUES (?, ?): '4', '10' |
1028 | |
1029 | |
1030 | =head2 Create a ResultSet Class |
1031 | |
55490817 |
1032 | An often overlooked but extremely powerful features of DBIC is that it |
1033 | allows you to supply your own subclasses of C<DBIx::Class::ResultSet>. |
1034 | It allows you to pull complex and unsightly "query code" out of your |
1cde0fd6 |
1035 | controllers and encapsulate it in a method of your ResultSet Class. |
1036 | These "canned queries" in your ResultSet Class can then be invoked |
1037 | via a single call, resulting in much cleaner and easier to read |
1038 | controller code. |
1039 | |
55490817 |
1040 | To illustrate the concept with a fairly simple example, let's create a |
1cde0fd6 |
1041 | method that returns books added in the last 10 minutes. Start by |
1042 | making a directory where DBIC will look for our ResultSet Class: |
1043 | |
1044 | mkdir lib/MyApp/Schema/ResultSet |
1045 | |
55490817 |
1046 | Then open C<lib/MyApp/Schema/ResultSet/Books.pm> and enter the following: |
1cde0fd6 |
1047 | |
1048 | package MyApp::Schema::ResultSet::Books; |
55490817 |
1049 | |
1cde0fd6 |
1050 | use strict; |
1051 | use warnings; |
1052 | use base 'DBIx::Class::ResultSet'; |
55490817 |
1053 | |
1cde0fd6 |
1054 | =head2 created_after |
55490817 |
1055 | |
1cde0fd6 |
1056 | A predefined search for recently added books |
55490817 |
1057 | |
1cde0fd6 |
1058 | =cut |
55490817 |
1059 | |
1cde0fd6 |
1060 | sub created_after { |
fadc4ae7 |
1061 | my ($self, $datetime) = @_; |
55490817 |
1062 | |
fadc4ae7 |
1063 | my $date_str = $self->_source_handle->schema->storage |
1064 | ->datetime_parser->format_datetime($datetime); |
55490817 |
1065 | |
fadc4ae7 |
1066 | return $self->search({ |
1067 | created => { '>' => $date_str } |
1068 | }); |
1cde0fd6 |
1069 | } |
55490817 |
1070 | |
1cde0fd6 |
1071 | 1; |
1072 | |
55490817 |
1073 | Then we need to tell the Result Class to to treat this as a ResultSet |
1074 | Class. Open C<lib/MyApp/Schema/Result/Books.pm> and add the following |
1cde0fd6 |
1075 | above the "C<1;>" at the bottom of the file: |
1076 | |
1077 | # |
1078 | # Set ResultSet Class |
1079 | # |
1080 | __PACKAGE__->resultset_class('MyApp::Schema::ResultSet::Books'); |
1081 | |
1082 | Then add the following method to the C<lib/MyApp/Controller/Books.pm>: |
1083 | |
1084 | =head2 list_recent |
55490817 |
1085 | |
1cde0fd6 |
1086 | List recently created books |
55490817 |
1087 | |
1cde0fd6 |
1088 | =cut |
55490817 |
1089 | |
1cde0fd6 |
1090 | sub list_recent :Chained('base') :PathPart('list_recent') :Args(1) { |
1091 | my ($self, $c, $mins) = @_; |
55490817 |
1092 | |
1cde0fd6 |
1093 | # Retrieve all of the book records as book model objects and store in the |
1094 | # stash where they can be accessed by the TT template, but only |
1095 | # retrieve books created within the last $min number of minutes |
1096 | $c->stash->{books} = [$c->model('DB::Books') |
1097 | ->created_after(DateTime->now->subtract(minutes => $mins))]; |
55490817 |
1098 | |
1cde0fd6 |
1099 | # Set the TT template to use. You will almost always want to do this |
1100 | # in your action methods (action methods respond to user input in |
1101 | # your controllers). |
1102 | $c->stash->{template} = 'books/list.tt2'; |
1103 | } |
1104 | |
55490817 |
1105 | Now start the development server with C<DBIC_TRACE=1> and try |
1106 | different values for the minutes argument (the final number value) for |
1107 | the URL C<http://localhost:3000/books/list_recent/10>. For example, |
1cde0fd6 |
1108 | this would list all books added in the last fifteen minutes: |
1109 | |
1110 | http://localhost:3000/books/list_recent/15 |
1111 | |
1112 | Depending on how recently you added books, you might want to |
1113 | try a higher or lower value. |
1114 | |
1115 | |
1116 | =head2 Chaining ResultSets |
1117 | |
72609296 |
1118 | One of the most helpful and powerful features in DBIC is that it allows |
1119 | you to "chain together" a series of queries (note that this has nothing |
1120 | to do with the "Chained Dispatch" for Catalyst that we were discussing |
1121 | above). Because each ResultSet returns another ResultSet, you can take |
1122 | an initial query and immediately feed that into a second query (and so |
1123 | on for as many queries you need). Note that no matter how many |
1124 | ResultSets you chain together, the database itself will not be hit until |
1125 | you use a method that attempts to access the data. And, because this |
1126 | technique carries over to the ResultSet Class feature we implemented in |
1127 | the previous section for our "canned search", we can combine the two |
1128 | capabilities. For example, let's add an action to our C<Books> |
55490817 |
1129 | controller that lists books that are both recent I<and> have "TCP" in |
1130 | the title. Open up C<lib/MyApp/Controller/Books.pm> and add the |
1cde0fd6 |
1131 | following method: |
1132 | |
acbd7bdd |
1133 | =head2 list_recent_tcp |
55490817 |
1134 | |
1cde0fd6 |
1135 | List recently created books |
55490817 |
1136 | |
1cde0fd6 |
1137 | =cut |
55490817 |
1138 | |
1cde0fd6 |
1139 | sub list_recent_tcp :Chained('base') :PathPart('list_recent_tcp') :Args(1) { |
1140 | my ($self, $c, $mins) = @_; |
55490817 |
1141 | |
1cde0fd6 |
1142 | # Retrieve all of the book records as book model objects and store in the |
1143 | # stash where they can be accessed by the TT template, but only |
1144 | # retrieve books created within the last $min number of minutes |
1145 | # AND that have 'TCP' in the title |
1146 | $c->stash->{books} = [$c->model('DB::Books') |
1147 | ->created_after(DateTime->now->subtract(minutes => $mins)) |
1148 | ->search({title => {'like', '%TCP%'}}) |
1149 | ]; |
55490817 |
1150 | |
1cde0fd6 |
1151 | # Set the TT template to use. You will almost always want to do this |
1152 | # in your action methods (action methods respond to user input in |
1153 | # your controllers). |
1154 | $c->stash->{template} = 'books/list.tt2'; |
1155 | } |
1156 | |
1157 | To try this out, restart the development server with: |
1158 | |
1159 | DBIC_TRACE=1 script/myapp_server.pl |
1160 | |
1161 | And enter the following URL into your browser: |
1162 | |
1163 | http://localhost:3000/books/list_recent_tcp/100 |
1164 | |
55490817 |
1165 | And you should get a list of books added in the last 100 minutes that |
1166 | contain the string "TCP" in the title. However, if you look at all |
1167 | books within the last 100 minutes, you should get a longer list |
1168 | (again, you might have to adjust the number of minutes depending on |
1cde0fd6 |
1169 | how recently you added books to your database): |
1170 | |
1171 | http://localhost:3000/books/list_recent/100 |
1172 | |
55490817 |
1173 | Take a look at the DBIC_TRACE output in the development server log for |
1cde0fd6 |
1174 | the first URL and you should see something similar to the following: |
1175 | |
55490817 |
1176 | SELECT me.id, me.title, me.rating, me.created, me.updated FROM books me |
acbd7bdd |
1177 | WHERE ( ( ( title LIKE ? ) AND ( created > ? ) ) ): '%TCP%', '2009-03-08 14:52:54' |
1cde0fd6 |
1178 | |
55490817 |
1179 | However, let's not pollute our controller code with this raw "TCP" |
1180 | query -- it would be cleaner to encapsulate that code in a method on |
1181 | our ResultSet Class. To do this, open |
1cde0fd6 |
1182 | C<lib/MyApp/Schema/ResultSet/Books.pm> and add the following method: |
1183 | |
1184 | =head2 title_like |
55490817 |
1185 | |
1cde0fd6 |
1186 | A predefined search for books with a 'LIKE' search in the string |
55490817 |
1187 | |
1cde0fd6 |
1188 | =cut |
55490817 |
1189 | |
1cde0fd6 |
1190 | sub title_like { |
fadc4ae7 |
1191 | my ($self, $title_str) = @_; |
55490817 |
1192 | |
fadc4ae7 |
1193 | return $self->search({ |
1194 | title => { 'like' => "%$title_str%" } |
1195 | }); |
1cde0fd6 |
1196 | } |
1197 | |
55490817 |
1198 | We defined the search string as C<$title_str> to make the method more |
1199 | flexible. Now update the C<list_recent_tcp> method in |
1200 | C<lib/MyApp/Controller/Books.pm> to match the following (we have |
1201 | replaced the C<-E<gt>search> line with the C<-E<gt>title_like> line |
1cde0fd6 |
1202 | shown here -- the rest of the method should be the same): |
1203 | |
1204 | =head2 list_recent_tcp |
55490817 |
1205 | |
1cde0fd6 |
1206 | List recently created books |
55490817 |
1207 | |
1cde0fd6 |
1208 | =cut |
55490817 |
1209 | |
1cde0fd6 |
1210 | sub list_recent_tcp :Chained('base') :PathPart('list_recent_tcp') :Args(1) { |
1211 | my ($self, $c, $mins) = @_; |
55490817 |
1212 | |
1cde0fd6 |
1213 | # Retrieve all of the book records as book model objects and store in the |
1214 | # stash where they can be accessed by the TT template, but only |
1215 | # retrieve books created within the last $min number of minutes |
1216 | # AND that have 'TCP' in the title |
1217 | $c->stash->{books} = [$c->model('DB::Books') |
1218 | ->created_after(DateTime->now->subtract(minutes => $mins)) |
1219 | ->title_like('TCP') |
1220 | ]; |
55490817 |
1221 | |
1cde0fd6 |
1222 | # Set the TT template to use. You will almost always want to do this |
1223 | # in your action methods (action methods respond to user input in |
1224 | # your controllers). |
1225 | $c->stash->{template} = 'books/list.tt2'; |
1226 | } |
1227 | |
55490817 |
1228 | Then restart the development server and try out the C<list_recent_tcp> |
1229 | and C<list_recent> URL as we did above. It should work just the same, |
1230 | but our code is obviously cleaner and more modular, while also being |
1cde0fd6 |
1231 | more flexible at the same time. |
1232 | |
1233 | |
1234 | =head2 Adding Methods to Result Classes |
1235 | |
55490817 |
1236 | In the previous two sections we saw a good example of how we could use |
1237 | DBIC ResultSet Classes to clean up our code for an entire query (for |
1238 | example, our "canned searches" that filtered the entire query). We |
1239 | can do a similar improvement when working with individual rows as |
1240 | well. Whereas the ResultSet construct is used in DBIC to correspond |
1241 | to an entire query, the Result Class construct is used to represent a |
1242 | row. Therefore, we can add row-specific "helper methods" to our Result |
1243 | Classes stored in C<lib/MyApp/Schema/Result/>. For example, open |
1244 | C<lib/MyApp/Schema/Result/Authors.pm> and add the following method |
1cde0fd6 |
1245 | (as always, it must be above the closing "C<1;>"): |
1246 | |
1247 | # |
1248 | # Helper methods |
1249 | # |
1250 | sub full_name { |
1251 | my ($self) = @_; |
55490817 |
1252 | |
1cde0fd6 |
1253 | return $self->first_name . ' ' . $self->last_name; |
1254 | } |
1255 | |
55490817 |
1256 | This will allow us to conveniently retrieve both the first and last |
1257 | name for an author in one shot. Now open C<root/src/books/list.tt2> |
1cde0fd6 |
1258 | and change the definition of C<tt_authors> from this: |
1259 | |
acbd7bdd |
1260 | ... |
1cde0fd6 |
1261 | [% tt_authors = [ ]; |
1262 | tt_authors.push(author.last_name) FOREACH author = book.authors %] |
acbd7bdd |
1263 | ... |
1cde0fd6 |
1264 | |
1265 | to: |
1266 | |
acbd7bdd |
1267 | ... |
1cde0fd6 |
1268 | [% tt_authors = [ ]; |
1269 | tt_authors.push(author.full_name) FOREACH author = book.authors %] |
acbd7bdd |
1270 | ... |
1cde0fd6 |
1271 | |
55490817 |
1272 | (Only C<author.last_name> was changed to C<author.full_name> -- the |
1cde0fd6 |
1273 | rest of the file should remain the same.) |
1274 | |
1275 | Now restart the development server and go to the standard book list |
1276 | URL: |
1277 | |
1278 | http://localhost:3000/books/list |
1279 | |
55490817 |
1280 | The "Author(s)" column will now contain both the first and last name. |
1281 | And, because the concatenation logic was encapsulated inside our |
1282 | Result Class, it keeps the code inside our .tt template nice and clean |
1283 | (remember, we want the templates to be as close to pure HTML markup as |
1284 | possible). Obviously, this capability becomes even more useful as you |
1285 | use to to remove even more complicated row-specific logic from your |
1cde0fd6 |
1286 | templates! |
1287 | |
1288 | |
d442cc9f |
1289 | =head1 AUTHOR |
1290 | |
1291 | Kennedy Clark, C<hkclark@gmail.com> |
1292 | |
1293 | Please report any errors, issues or suggestions to the author. The |
1294 | most recent version of the Catalyst Tutorial can be found at |
82ab4bbf |
1295 | L<http://dev.catalyst.perl.org/repos/Catalyst/Catalyst-Manual/5.70/trunk/lib/Catalyst/Manual/Tutorial/>. |
d442cc9f |
1296 | |
45c7830f |
1297 | Copyright 2006-2008, Kennedy Clark, under Creative Commons License |
95674086 |
1298 | (L<http://creativecommons.org/licenses/by-sa/3.0/us/>). |