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