Merge from depluralization branch
[catagits/Catalyst-Manual.git] / lib / Catalyst / Manual / Tutorial / BasicCRUD.pod
index 72be8e1..463e8d5 100644 (file)
@@ -1,11 +1,11 @@
 =head1 NAME
 
-Catalyst::Manual::Tutorial::BasicCRUD - Catalyst Tutorial - Part 4: Basic CRUD
+Catalyst::Manual::Tutorial::BasicCRUD - Catalyst Tutorial - Chapter 4: Basic CRUD
 
 
 =head1 OVERVIEW
 
-This is B<Part 4 of 10> for the Catalyst tutorial.
+This is B<Chapter 4 of 10> for the Catalyst tutorial.
 
 L<Tutorial Overview|Catalyst::Manual::Tutorial>
 
@@ -56,32 +56,32 @@ L<Appendices|Catalyst::Manual::Tutorial::Appendices>
 
 =head1 DESCRIPTION
 
-This part of the tutorial builds on the fairly primitive application
-created in Part 3 to add basic support for Create, Read, Update, and
-Delete (CRUD) of C<Book> objects.  Note that the 'list' function in Part
-2 already implements the Read portion of CRUD (although Read normally
-refers to reading a single object; you could implement full read
-functionality using the techniques introduced below).  This section will
-focus on the Create and Delete aspects of CRUD.  More advanced
-capabilities, including full Update functionality, will be addressed in
-Part 9.
-
-Although this part of the tutorial will show you how to build CRUD 
+This chapter of the tutorial builds on the fairly primitive 
+application created in Chapter 3 to add basic support for Create, 
+Read, Update, and Delete (CRUD) of C<Book> objects.  Note that the 
+'list' function in Chapter 2 already implements the Read portion of 
+CRUD (although Read normally refers to reading a single object; you 
+could implement full Read functionality using the techniques 
+introduced below).  This section will focus on the Create and Delete 
+aspects of CRUD.  More advanced capabilities, including full Update 
+functionality, will be addressed in Chapter 9.
+
+Although this chapter of the tutorial will show you how to build CRUD 
 functionality yourself, another option is to use a "CRUD builder" type 
 of tool to automate the process.  You get less control, but it's quick 
 and easy.  For example, see 
-L<CatalystX::ListFramework::Builder|CatalystX::ListFramework::Builder>,
+L<CatalystX::ListFramework::Builder|CatalystX::ListFramework::Builder>, 
 L<CatalystX::CRUD|CatalystX::CRUD>, and 
 L<CatalystX::CRUD::YUI|CatalystX::CRUD::YUI>.
 
-You can checkout the source code for this example from the catalyst
-subversion repository as per the instructions in
+You can check out the source code for this example from the Catalyst
+Subversion repository as per the instructions in
 L<Catalyst::Manual::Tutorial::Intro|Catalyst::Manual::Tutorial::Intro>.
 
 
 =head1 FORMLESS SUBMISSION
 
-Our initial attempt at object creation will utilize the "URL 
+Our initial attempt at object creation will utilize the "URL
 arguments" feature of Catalyst (we will employ the more common form-
 based submission in the sections that follow).
 
@@ -91,42 +91,34 @@ based submission in the sections that follow).
 Edit C<lib/MyApp/Controller/Books.pm> and enter the following method:
 
     =head2 url_create
-    
+
     Create a book with the supplied title, rating, and author
-    
+
     =cut
-    
+
     sub url_create : Local {
-        # In addition to self & context, get the title, rating, & 
-        # author_id args from the URL.  Note that Catalyst automatically 
-        # puts extra information after the "/<controller_name>/<action_name/" 
+        # In addition to self & context, get the title, rating, &
+        # author_id args from the URL.  Note that Catalyst automatically
+        # puts extra information after the "/<controller_name>/<action_name/"
         # into @_
         my ($self, $c, $title, $rating, $author_id) = @_;
-    
-        # Call create() on the book model object. Pass the table 
+
+        # Call create() on the book model object. Pass the table
         # columns/field values we want to set as hash values
-        my $book = $c->model('DB::Books')->create({
+        my $book = $c->model('DB::Book')->create({
                 title  => $title,
                 rating => $rating
             });
-        
-        # Add a record to the join table for this book, mapping to 
+
+        # Add a record to the join table for this book, mapping to
         # appropriate author
-        $book->add_to_book_authors({author_id => $author_id});
+        $book->add_to_book_author({author_id => $author_id});
         # Note: Above is a shortcut for this:
-        # $book->create_related('book_authors', {author_id => $author_id});
-        
+        # $book->create_related('book_author', {author_id => $author_id});
+
         # Assign the Book object to the stash for display in the view
         $c->stash->{book} = $book;
-    
-        # This is a hack to disable XSUB processing in Data::Dumper
-        # (it's used in the view).  This is a work-around for a bug in
-        # the interaction of some versions or Perl, Data::Dumper & DBIC.
-        # You won't need this if you aren't using Data::Dumper (or if
-        # you are running DBIC 0.06001 or greater), but adding it doesn't 
-        # hurt anything either.
-        $Data::Dumper::Useperl = 1;
-    
+
         # Set the TT template to use
         $c->stash->{template} = 'books/create_done.tt2';
     }
@@ -135,12 +127,12 @@ Notice that Catalyst takes "extra slash-separated information" from the
 URL and passes it as arguments in C<@_>.  The C<url_create> action then
 uses a simple call to the DBIC C<create> method to add the requested
 information to the database (with a separate call to
-C<add_to_book_authors> to update the join table).  As do virtually all
+C<add_to_book_author> to update the join table).  As do virtually all
 controller methods (at least the ones that directly handle user input),
 it then sets the template that should handle this request.
 
 
-=head2 Include a Template for the C<url_create> Action:
+=head2 Include a Template for the 'url_create' Action:
 
 Edit C<root/src/books/create_done.tt2> and then enter:
 
@@ -148,47 +140,48 @@ Edit C<root/src/books/create_done.tt2> and then enter:
     [% # Not a good idea for production use, though. :-)  'Indent=1' is      -%]
     [% # optional, but prevents "massive indenting" of deeply nested objects -%]
     [% USE Dumper(Indent=1) -%]
-    
+
     [% # Set the page title.  META can 'go back' and set values in templates -%]
     [% # that have been processed 'before' this template (here it's for      -%]
-    [% # root/lib/site/html and root/lib/site/header).  Note that META on    -%]
-    [% # simple strings (e.g., no variable interpolation).                   -%]
+    [% # root/lib/site/html and root/lib/site/header).  Note that META only  -%]
+    [% # works on simple/static strings (i.e. there is no variable           -%]
+    [% # interpolation).                                                     -%]
     [% META title = 'Book Created' %]
-    
+
     [% # Output information about the record that was added.  First title.       -%]
     <p>Added book '[% book.title %]'
-    
+
     [% # Output the last name of the first author.  This is complicated by an    -%]
     [% # issue in TT 2.15 where blessed hash objects are not handled right.      -%]
-    [% # First, fetch 'book.authors' from the DB once.                           -%]
-    [% authors = book.authors %]
+    [% # First, fetch 'book.author' from the DB once.                           -%]
+    [% authors = book.author %]
     [% # Now use IF statements to test if 'authors.first' is "working". If so,   -%]
     [% # we use it.  Otherwise we use a hack that seems to keep TT 2.15 happy.   -%]
-    by '[% authors.first.last_name IF authors.first; 
+    by '[% authors.first.last_name IF authors.first;
            authors.list.first.value.last_name IF ! authors.first %]'
-    
+
     [% # Output the rating for the book that was added -%]
     with a rating of [% book.rating %].</p>
-    
+
     [% # Provide a link back to the list page                                    -%]
     [% # 'uri_for()' builds a full URI; e.g., 'http://localhost:3000/books/list' -%]
     <p><a href="[% c.uri_for('/books/list') %]">Return to list</a></p>
-    
+
     [% # Try out the TT Dumper (for development only!) -%]
     <pre>
     Dump of the 'book' variable:
     [% Dumper.dump(book) %]
     </pre>
 
-The TT C<USE> directive allows access to a variety of plugin modules 
-(TT plugins, that is, not Catalyst plugins) to add extra functionality 
-to the base TT capabilities.  Here, the plugin allows 
-L<Data::Dumper|Data::Dumper> "pretty printing" of objects and 
-variables.  Other than that, the rest of the code should be familiar 
-from the examples in Part 3.
+The TT C<USE> directive allows access to a variety of plugin modules
+(TT plugins, that is, not Catalyst plugins) to add extra functionality
+to the base TT capabilities.  Here, the plugin allows
+L<Data::Dumper|Data::Dumper> "pretty printing" of objects and
+variables.  Other than that, the rest of the code should be familiar
+from the examples in Chapter 3.
 
 
-=head2 Try the C<url_create> Feature
+=head2 Try the 'url_create' Feature
 
 If the application is still running from before, use C<Ctrl-C> to kill
 it. Then restart the server:
@@ -209,34 +202,253 @@ Next, use your browser to enter the following URL:
 
     http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4
 
-Your browser should display " Added book 'TCPIP_Illustrated_Vol-2' by
+Your browser should display "Added book 'TCPIP_Illustrated_Vol-2' by
 'Stevens' with a rating of 5." along with a dump of the new book model
-object.  You should also see the following DBIC debug messages displayed
-in the development server log messages if you have DBIC_TRACE set:
+object as it was returned by DBIC.  You should also see the following
+DBIC debug messages displayed in the development server log messages
+if you have DBIC_TRACE set:
 
-    INSERT INTO books (rating, title) VALUES (?, ?): `5', `TCPIP_Illustrated_Vol-2'
-    INSERT INTO book_authors (author_id, book_id) VALUES (?, ?): `4', `6'
-    SELECT author.id, author.first_name, author.last_name 
-        FROM book_authors me  JOIN authors author 
-        ON ( author.id = me.author_id ) WHERE ( me.book_id = ? ): '6'
+    INSERT INTO book (rating, title) VALUES (?, ?): `5', `TCPIP_Illustrated_Vol-2'
+    INSERT INTO book_author (author_id, book_id) VALUES (?, ?): `4', `6'
 
 The C<INSERT> statements are obviously adding the book and linking it to
 the existing record for Richard Stevens.  The C<SELECT> statement results
 from DBIC automatically fetching the book for the C<Dumper.dump(book)>.
 
-If you then click the "Return to list" link, you should find that 
-there are now six books shown (if necessary, Shift+Reload or 
-Ctrl+Reload your browser at the C</books/list> page).
+If you then click the "Return to list" link, you should find that
+there are now six books shown (if necessary, Shift+Reload or
+Ctrl+Reload your browser at the C</books/list> page).  You should now see
+the following six DBIC debug messages displayed for N=1-6:
+
+    SELECT author.id, author.first_name, author.last_name \
+        FROM book_author me  JOIN author author \
+        ON ( author.id = me.author_id ) WHERE ( me.book_id = ? ): 'N'
+
+
+=head1 CONVERT TO A CHAINED ACTION
+
+Although the example above uses the same C<Local> action type for the
+method that we saw in the previous chapter of the tutorial, there is an
+alternate approach that allows us to be more specific while also
+paving the way for more advanced capabilities.  Change the method
+declaration for C<url_create> in C<lib/MyApp/Controller/Books.pm> you
+entered above to match the following:
+
+    sub url_create :Chained('/') :PathPart('books/url_create') :Args(3) {
+
+This converts the method to take advantage of the Chained
+action/dispatch type. Chaining lets you have a single URL
+automatically dispatch to several controller methods, each of which
+can have precise control over the number of arguments that it will
+receive.  A chain can essentially be thought of having three parts --
+a beginning, a middle, and an end.  The bullets below summarize the key
+points behind each of these parts of a chain:
+
+
+=over 4
+
+
+=item *
+
+Beginning
+
+=over 4
+
+=item *
+
+B<Use "C<:Chained('/')>" to start a chain>
+
+=item *
+
+Get arguments through C<CaptureArgs()>
 
-Then I<add 2 more copies of the same book> so that we have some extras for
-our delete logic that will be coming up soon.  Enter the same URL above
-two more times (or refresh your browser twice if it still contains this
-URL):
+=item *
+
+Specify the path to match with C<PathPart()>
+
+=back
+
+
+=item *
+
+Middle
+
+=over 4
+
+=item *
+
+Link to previous part of the chain with C<:Chained('_name_')>
+
+=item *
+
+Get arguments through C<CaptureArgs()>
+
+=item *
+
+Specify the path to match with C<PathPart()>
+
+=back
+
+
+=item *
+
+End
+
+=over 4
+
+=item *
+
+Link to previous part of the chain with C<:Chained('_name_')>
+
+=item *
+
+B<Do NOT get arguments through "C<CaptureArgs()>," use "C<Args()>" instead to end a chain>
+
+=item *
+
+Specify the path to match with C<PathPart()>
+
+=back
+
+
+=back
+
+In our C<url_create> method above, we have combined all three parts into
+a single method: C<:Chained('/')> to start the chain,
+C<:PathPart('books/url_create')> to specify the base URL to match, and
+C<:Args(3)> to capture exactly three arguments and to end the chain.
+
+As we will see shortly, a chain can consist of as many "links" as you
+wish, with each part capturing some arguments and doing some work
+along the way.  We will continue to use the Chained action type in this
+chapter of the tutorial and explore slightly more advanced capabilities
+with the base method and delete feature below.  But Chained dispatch
+is capable of far more.  For additional information, see
+L<Catalyst::Manual::Intro/Action types>,
+L<Catalyst::DispatchType::Chained|Catalyst::DispatchType::Chained>,
+and the 2006 Advent calendar entry on the subject:
+L<http://www.catalystframework.org/calendar/2006/10>.
+
+
+=head2 Try the Chained Action
+
+If you look back at the development server startup logs from your
+initial version of the C<url_create> method (the one using the
+C<:Local> attribute), you will notice that it produced output similar
+to the following:
+
+    [debug] Loaded Path actions:
+    .-------------------------------------+--------------------------------------.
+    | Path                                | Private                              |
+    +-------------------------------------+--------------------------------------+
+    | /                                   | /default                             |
+    | /                                   | /index                               |
+    | /books                              | /books/index                         |
+    | /books/list                         | /books/list                          |
+    | /books/url_create                   | /books/url_create                    |
+    '-------------------------------------+--------------------------------------'
+
+Now start the development server with our basic chained method in
+place and the startup debug output should change to something along
+the lines of the following:
+
+    [debug] Loaded Path actions:
+    .-------------------------------------+--------------------------------------.
+    | Path                                | Private                              |
+    +-------------------------------------+--------------------------------------+
+    | /                                   | /default                             |
+    | /                                   | /index                               |
+    | /books                              | /books/index                         |
+    | /books/list                         | /books/list                          |
+    '-------------------------------------+--------------------------------------'
+
+    [debug] Loaded Chained actions:
+    .-------------------------------------+--------------------------------------.
+    | Path Spec                           | Private                              |
+    +-------------------------------------+--------------------------------------+
+    | /books/url_create/*/*/*             | /books/url_create                    |
+    '-------------------------------------+--------------------------------------'
+
+C<url_create> has disappeared form the "Loaded Path actions" section
+but it now shows up under the newly created "Loaded Chained actions"
+section.  And the "/*/*/*" portion clearly shows our requirement for
+three arguments.
+
+As with our non-chained version of C<url_create>, use your browser to
+enter the following URL:
+
+    http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4
+
+You should see the same "Added book 'TCPIP_Illustrated_Vol-2' by
+'Stevens' with a rating of 5." along with a dump of the new book model
+object.  Click the "Return to list" link, and you should find that there
+are now seven books shown (two copies of I<TCPIP_Illustrated_Vol-2>).
+
+
+=head2 Refactor to Use a 'base' Method to Start the Chains
+
+Let's make a quick update to our initial Chained action to show a
+little more of the power of chaining.  First, open
+C<lib/MyApp/Controller/Books.pm> in your editor and add the following
+method:
+
+    =head2 base
+
+    Can place common logic to start chained dispatch here
+
+    =cut
+
+    sub base :Chained('/') :PathPart('books') :CaptureArgs(0) {
+        my ($self, $c) = @_;
+
+        # Store the ResultSet in stash so it's available for other methods
+        $c->stash->{resultset} = $c->model('DB::Book');
+
+        # Print a message to the debug log
+        $c->log->debug('*** INSIDE BASE METHOD ***');
+    }
+
+Here we print a log message and store the DBIC ResultSet in
+C<$c-E<gt>stash-E<gt>{resultset}> so that it's automatically available
+for other actions that chain off C<base>.  If your controller always
+needs a book ID as its first argument, you could have the base method
+capture that argument (with C<:CaptureArgs(1)>) and use it to pull the
+book object with C<-E<gt>find($id)> and leave it in the stash for
+later parts of your chains to then act upon. Because we have several
+actions that don't need to retrieve a book (such as the C<url_create>
+we are working with now), we will instead add that functionality
+to a common C<object> action shortly.
+
+As for C<url_create>, let's modify it to first dispatch to C<base>.
+Open up C<lib/MyApp/Controller/Books.pm> and edit the declaration for
+C<url_create> to match the following:
+
+    sub url_create :Chained('base') :PathPart('url_create') :Args(3) {
+
+Next, try out the refactored chain by restarting the development
+server.  Notice that our "Loaded Chained actions" section has changed
+slightly:
+
+    [debug] Loaded Chained actions:
+    .-------------------------------------+--------------------------------------.
+    | Path Spec                           | Private                              |
+    +-------------------------------------+--------------------------------------+
+    | /books/url_create/*/*/*             | /books/base (0)                      |
+    |                                     | => /books/url_create                 |
+    '-------------------------------------+--------------------------------------'
+
+The "Path Spec" is the same, but now it maps to two Private actions as
+we would expect.
+
+Once again, enter the following URL into your browser:
 
     http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4
 
-You should be able to click "Return to list" and now see 3 copies of 
-"TCP_Illustrated_Vol-2".
+The same "Added book 'TCPIP_Illustrated_Vol-2' by 'Stevens' with a
+rating of 5." message and a dump of the new book object should appear.
+Also notice the extra debug message in the development server output
+from the C<base> method.  Click the "Return to list" link, and you
+should find that there are now eight books shown.
 
 
 =head1 MANUALLY BUILDING A CREATE FORM
@@ -252,19 +464,19 @@ to enter data.  This section begins to address that concern.
 Edit C<lib/MyApp/Controller/Books.pm> and add the following method:
 
     =head2 form_create
-    
+
     Display form to collect information for book to create
-    
+
     =cut
-    
-    sub form_create : Local {
+
+    sub form_create :Chained('base') :PathPart('form_create') :Args(0) {
         my ($self, $c) = @_;
-    
+
         # Set the TT template to use
         $c->stash->{template} = 'books/form_create.tt2';
     }
 
-This action simply invokes a view containing a book creation form.
+This action simply invokes a view containing a form to create a book.
 
 
 =head2 Add a Template for the Form
@@ -272,7 +484,7 @@ This action simply invokes a view containing a book creation form.
 Open C<root/src/books/form_create.tt2> in your editor and enter:
 
     [% META title = 'Manual Form Book Create' -%]
-    
+
     <form method="post" action="[% c.uri_for('form_create_do') %]">
     <table>
       <tr><td>Title:</td><td><input type="text" name="title"></td></tr>
@@ -292,34 +504,34 @@ Edit C<lib/MyApp/Controller/Books.pm> and add the following method to
 save the form information to the database:
 
     =head2 form_create_do
-    
+
     Take information from form and add to database
-    
+
     =cut
-    
-    sub form_create_do : Local {
+
+    sub form_create_do :Chained('base') :PathPart('form_create_do') :Args(0) {
         my ($self, $c) = @_;
-    
+
         # Retrieve the values from the form
         my $title     = $c->request->params->{title}     || 'N/A';
         my $rating    = $c->request->params->{rating}    || 'N/A';
         my $author_id = $c->request->params->{author_id} || '1';
-    
+
         # Create the book
-        my $book = $c->model('DB::Books')->create({
+        my $book = $c->model('DB::Book')->create({
                 title   => $title,
                 rating  => $rating,
             });
         # Handle relationship with author
-        $book->add_to_book_authors({author_id => $author_id});
-    
+        $book->add_to_book_author({author_id => $author_id});
+
         # Store new model object in stash
         $c->stash->{book} = $book;
-    
+
         # Avoid Data::Dumper issue mentioned earlier
-        # You can probably omit this    
+        # You can probably omit this
         $Data::Dumper::Useperl = 1;
-    
+
         # Set the TT template to use
         $c->stash->{template} = 'books/create_done.tt2';
     }
@@ -332,6 +544,21 @@ it.  Then restart the server:
 
     $ script/myapp_server.pl
 
+Notice that the server startup log reflects the two new chained
+methods that we added:
+
+    [debug] Loaded Chained actions:
+    .-------------------------------------+--------------------------------------.
+    | Path Spec                           | Private                              |
+    +-------------------------------------+--------------------------------------+
+    | /books/form_create                  | /books/base (0)                      |
+    |                                     | => /books/form_create                |
+    | /books/form_create_do               | /books/base (0)                      |
+    |                                     | => /books/form_create_do             |
+    | /books/url_create/*/*/*             | /books/base (0)                      |
+    |                                     | => /books/url_create                 |
+    '-------------------------------------+--------------------------------------'
+
 Point your browser to L<http://localhost:3000/books/form_create> and
 enter "TCP/IP Illustrated, Vol 3" for the title, a rating of 5, and an
 author ID of 4.  You should then see the output of the same
@@ -340,30 +567,30 @@ C<create_done.tt2> template seen in earlier examples.  Finally, click
 
 B<Note:> Having the user enter the primary key ID for the author is
 obviously crude; we will address this concern with a drop-down list in
-Part 9.
+Chapter 9.
 
 
 =head1 A SIMPLE DELETE FEATURE
 
-Turning our attention to the delete portion of CRUD, this section
+Turning our attention to the Delete portion of CRUD, this section
 illustrates some basic techniques that can be used to remove information
 from the database.
 
 
 =head2 Include a Delete Link in the List
 
-Edit C<root/src/books/list.tt2> and update it to the following (two
+Edit C<root/src/books/list.tt2> and update it to match the following (two
 sections have changed: 1) the additional '<th>Links</th>' table header,
-and 2) the four lines for the Delete link near the bottom).
+and 2) the four lines for the Delete link near the bottom):
 
     [% # This is a TT comment.  The '-' at the end "chomps" the newline.  You won't -%]
     [% # see this "chomping" in your browser because HTML ignores blank lines, but  -%]
     [% # it WILL eliminate a blank line if you view the HTML source.  It's purely   -%]
     [%- # optional, but both the beginning and the ending TT tags support chomping. -%]
-    
-    [% # Provide a title to root/lib/site/header -%]
+
+    [% # Provide a title -%]
     [% META title = 'Book List' -%]
-    
+
     <table>
     <tr><th>Title</th><th>Rating</th><th>Author(s)</th><th>Links</th></tr>
     [% # Display each book in a table row %]
@@ -374,21 +601,22 @@ and 2) the four lines for the Delete link near the bottom).
         <td>
           [% # First initialize a TT variable to hold a list.  Then use a TT FOREACH -%]
           [% # loop in 'side effect notation' to load just the last names of the     -%]
-          [% # authors into the list.  Note that the 'push' TT vmethod does not      -%]
+          [% # authors into the list. Note that the 'push' TT vmethod doesn't return -%]
           [% # a value, so nothing will be printed here.  But, if you have something -%]
-          [% # in TT that does return a method and you don't want it printed, you    -%]
-          [% # can: 1) assign it to a bogus value, or 2) use the CALL keyword to     -%]
+          [% # in TT that does return a value and you don't want it printed, you can -%]
+          [% # 1) assign it to a bogus value, or # 2) use the CALL keyword to        -%]
           [% # call it and discard the return value.                                 -%]
           [% tt_authors = [ ];
-             tt_authors.push(author.last_name) FOREACH author = book.authors %]
+             tt_authors.push(author.last_name) FOREACH author = book.author %]
           [% # Now use a TT 'virtual method' to display the author count in parens   -%]
-          ([% tt_authors.size %])
+          [% # Note the use of the TT filter "| html" to escape dangerous characters -%]
+          ([% tt_authors.size | html %])
           [% # Use another TT vmethod to join & print the names & comma separators   -%]
-          [% tt_authors.join(', ') %]
+          [% tt_authors.join(', ') | html %]
         </td>
         <td>
           [% # Add a link to delete a book %]
-          <a href="[% c.uri_for('delete', book.id) %]">Delete</a>
+          <a href="[% c.uri_for(c.controller.action_for('delete'), [book.id]) %]">Delete</a>
         </td>
       </tr>
     [% END -%]
@@ -396,7 +624,97 @@ and 2) the four lines for the Delete link near the bottom).
 
 The additional code is obviously designed to add a new column to the
 right side of the table with a C<Delete> "button" (for simplicity, links
-will be used instead of full HTML buttons).
+will be used instead of full HTML buttons; in practice, anything that
+modifies data should be handled with a form sending a PUT request).
+
+Also notice that we are using a more advanced form of C<uri_for> than
+we have seen before.  Here we use
+C<$c-E<gt>controller-E<gt>action_for> to automatically generate a URI
+appropriate for that action based on the method we want to link to
+while inserting the C<book.id> value into the appropriate place.  Now,
+if you ever change C<:PathPart('delete')> in your controller method to
+C<:PathPart('kill')>, then your links will automatically update
+without any changes to your .tt2 template file.  As long as the name
+of your method does not change (here, "delete"), then your links will
+still be correct.  There are a few shortcuts and options when using
+C<action_for()>:
+
+=over 4
+
+=item *
+
+If you are referring to a method in the current controller, you can
+use C<$self-E<gt>action_for('_method_name_')>.
+
+=item *
+
+If you are referring to a method in a different controller, you need
+to include that controller's name as an argument to C<controller()>, as in
+C<$c-E<gt>controller('_controller_name_')-E<gt>action_for('_method_name_')>.
+
+=back
+
+B<Note:> In practice you should B<never> use a GET request to delete a
+record -- always use POST for actions that will modify data.  We are
+doing it here for illustrative and simplicity purposes only.
+
+
+=head2 Add a Common Method to Retrieve a Book for the Chain
+
+As mentioned earlier, since we have a mixture of actions that operate
+on a single book ID and others that do not, we should not have C<base>
+capture the book ID, find the corresponding book in the database and
+save it in the stash for later links in the chain.  However, just
+because that logic does not belong in C<base> doesn't mean that we
+can't create another location to centralize the book lookup code.  In
+our case, we will create a method called C<object> that will store the
+specific book in the stash.  Chains that always operate on a single
+existing book can chain off this method, but methods such as
+C<url_create> that don't operate on an existing book can chain
+directly off base.
+
+To add the C<object> method, edit C<lib/MyApp/Controller/Books.pm>
+and add the following code:
+
+    =head2 object
+
+    Fetch the specified book object based on the book ID and store
+    it in the stash
+
+    =cut
+
+    sub object :Chained('base') :PathPart('id') :CaptureArgs(1) {
+        # $id = primary key of book to delete
+        my ($self, $c, $id) = @_;
+
+        # Find the book object and store it in the stash
+        $c->stash(object => $c->stash->{resultset}->find($id));
+
+        # Make sure the lookup was successful.  You would probably
+        # want to do something like this in a real app:
+        #   $c->detach('/error_404') if !$c->stash->{object};
+        die "Book $id not found!" if !$c->stash->{object};
+    }
+
+Now, any other method that chains off C<object> will automatically
+have the appropriate book waiting for it in
+C<$c-E<gt>stash-E<gt>{object}>.
+
+Also note that we are using a different technique for setting
+C<$c-E<gt>stash>.  The advantage of this style is that it lets you set
+multiple stash variables at a time.  For example:
+
+    $c->stash(object => $c->stash->{resultset}->find($id),
+              another_thing => 1);
+
+or as a hashref:
+
+    $c->stash({object => $c->stash->{resultset}->find($id),
+              another_thing => 1});
+
+Either format works, but the C<$c-E<gt>stash(name =E<gt> value);>
+style is growing in popularity -- you may wish to use it all
+the time (even when you are only setting a single value).
 
 
 =head2 Add a Delete Action to the Controller
@@ -405,31 +723,28 @@ Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
 following method:
 
     =head2 delete
-    
+
     Delete a book
-        
+
     =cut
-    
-    sub delete : Local {
-        # $id = primary key of book to delete
-        my ($self, $c, $id) = @_;
-    
-        # Search for the book and then delete it
-        $c->model('DB::Books')->search({id => $id})->delete_all;
-    
+
+    sub delete :Chained('object') :PathPart('delete') :Args(0) {
+        my ($self, $c) = @_;
+
+        # Use the book object saved by 'object' and delete it along
+        # with related 'book_author' entries
+        $c->stash->{object}->delete;
+
         # Set a status message to be displayed at the top of the view
         $c->stash->{status_msg} = "Book deleted.";
-    
+
         # Forward to the list action/method in this controller
         $c->forward('list');
     }
 
-This method first deletes the book with the specified primary key ID.
+This method first deletes the book object saved by the C<object> method.
 However, it also removes the corresponding entry from the
-C<book_authors> table.  Note that C<delete_all> was used instead of
-C<delete>: whereas C<delete_all> also removes the join table entries in
-C<book_authors>, C<delete> does not (only use C<delete_all> if you
-really need the cascading deletes... otherwise you are wasting resources).
+C<book_author> table with a cascading delete.
 
 Then, rather than forwarding to a "delete done" page as we did with the
 earlier create example, it simply sets the C<status_msg> to display a
@@ -447,24 +762,50 @@ equivalent.
 If the application is still running from before, use C<Ctrl-C> to kill
 it.  Then restart the server:
 
-    $ script/myapp_server.pl
+    $ DBIC_TRACE=1 script/myapp_server.pl
+
+The C<delete> method now appears in the "Loaded Chained actions" section
+of the startup debug output:
+
+    [debug] Loaded Chained actions:
+    .-------------------------------------+--------------------------------------.
+    | Path Spec                           | Private                              |
+    +-------------------------------------+--------------------------------------+
+    | /books/id/*/delete                  | /books/base (0)                      |
+    |                                     | -> /books/object (1)                 |
+    |                                     | => /books/delete                     |
+    | /books/form_create                  | /books/base (0)                      |
+    |                                     | => /books/form_create                |
+    | /books/form_create_do               | /books/base (0)                      |
+    |                                     | => /books/form_create_do             |
+    | /books/url_create/*/*/*             | /books/base (0)                      |
+    |                                     | => /books/url_create                 |
+    '-------------------------------------+--------------------------------------'
 
 Then point your browser to L<http://localhost:3000/books/list> and click
-the "Delete" link next to the first "TCPIP_Illustrated_Vol-2".  A green 
-"Book deleted" status message should display at the top of the page, 
-along with a list of the eight remaining books.
+the "Delete" link next to the first "TCPIP_Illustrated_Vol-2".  A green
+"Book deleted" status message should display at the top of the page,
+along with a list of the eight remaining books.  You will also see the
+cascading delete operation via the DBIC_TRACE output:
+
+    SELECT me.id, me.title, me.rating FROM book me WHERE ( ( me.id = ? ) ): '6'
+    DELETE FROM book WHERE ( id = ? ): '6'
+    SELECT me.book_id, me.author_id FROM book_author me WHERE ( me.book_id = ? ): '6'
+    DELETE FROM book_author WHERE ( author_id = ? AND book_id = ? ): '4', '6'
 
 
 =head2 Fixing a Dangerous URL
 
-Note the URL in your browser once you have performed the deletion in the 
+Note the URL in your browser once you have performed the deletion in the
 prior step -- it is still referencing the delete action:
 
-    http://localhost:3000/books/delete/6
+    http://localhost:3000/books/id/6/delete
 
 What if the user were to press reload with this URL still active?  In
-this case the redundant delete is harmless, but in other cases this
-could clearly be extremely dangerous.
+this case the redundant delete is harmless (although it does generate
+an exception screen, it doesn't perform any undesirable actions on the
+application or database), but in other cases this could clearly be
+extremely dangerous.
 
 We can improve the logic by converting to a redirect.  Unlike
 C<$c-E<gt>forward('list'))> or C<$c-E<gt>detach('list'))> that perform
@@ -474,72 +815,75 @@ new request.  As a result, the URL in the browser is updated to match
 the destination of the redirection URL.
 
 To convert the forward used in the previous section to a redirect,
-open C<lib/MyApp/Controller/Books.pm> and edit the existing 
+open C<lib/MyApp/Controller/Books.pm> and edit the existing
 C<sub delete> method to match:
 
-    =head2 delete 
-    
+    =head2 delete
+
     Delete a book
-        
+
     =cut
-    
-    sub delete : Local {
-        # $id = primary key of book to delete
-        my ($self, $c, $id) = @_;
-    
-        # Search for the book and then delete it
-        $c->model('DB::Books')->search({id => $id})->delete_all;
-    
+
+    sub delete :Chained('object') :PathPart('delete') :Args(0) {
+        my ($self, $c) = @_;
+
+        # Use the book object saved by 'object' and delete it along
+        # with related 'book_author' entries
+        $c->stash->{object}->delete;
+
         # Set a status message to be displayed at the top of the view
         $c->stash->{status_msg} = "Book deleted.";
-    
-        # Redirect the user back to the list page
-        $c->response->redirect($c->uri_for('/books/list'));
+
+        # Redirect the user back to the list page.  Note the use
+        # of $self->action_for as earlier in this section (BasicCRUD)
+        $c->response->redirect($c->uri_for($self->action_for('list')));
     }
 
 
 =head2 Try the Delete and Redirect Logic
 
-Restart the development server and point your browser to 
-L<http://localhost:3000/books/list> and delete the first copy of 
-"TCPIP_Illustrated_Vol-2".  The URL in your browser should return to 
-the L<http://localhost:3000/books/list> URL, so that is an 
-improvement, but notice that I<no green "Book deleted" status message 
-is displayed>.  Because the stash is reset on every request (and a 
-redirect involves a second request), the C<status_msg> is cleared 
-before it can be displayed.
+Restart the development server and point your browser to
+L<http://localhost:3000/books/list> (don't just hit "Refresh" in your
+browser since we left the URL in an invalid state in the previous
+section!) and delete the first copy of the remaining two
+"TCPIP_Illustrated_Vol-2" books.  The URL in your browser should return
+to the L<http://localhost:3000/books/list> URL, so that is an
+improvement, but notice that I<no green "Book deleted" status message is
+displayed>.  Because the stash is reset on every request (and a redirect
+involves a second request), the C<status_msg> is cleared before it can
+be displayed.
+
 
+=head2 Using 'uri_for' to Pass Query Parameters
 
-=head2 Using C<uri_for> to Pass Query Parameters
+There are several ways to pass information across a redirect. One 
+option is to use the C<flash> technique that we will see in Chapter 5 
+of this tutorial; however, here we will pass the information via query 
+parameters on the redirect itself.  Open 
+C<lib/MyApp/Controller/Books.pm> and update the existing C<sub delete> 
+method to match the following:
 
-There are several ways to pass information across a redirect. 
-In general, the best option is to use the C<flash> technique that we
-will see in Part 5 of the tutorial; however, here we will pass the
-information via query parameters on the redirect itself.  Open 
-C<lib/MyApp/Controller/Books.pm> and update the existing 
-C<sub delete> method to match the following:
+    =head2 delete
 
-    =head2 delete 
-    
     Delete a book
-        
+
     =cut
-    
-    sub delete : Local {
-        # $id = primary key of book to delete
-        my ($self, $c, $id) = @_;
-    
-        # Search for the book and then delete it
-        $c->model('DB::Books')->search({id => $id})->delete_all;
-    
+
+    sub delete :Chained('object') :PathPart('delete') :Args(0) {
+        my ($self, $c) = @_;
+
+        # Use the book object saved by 'object' and delete it along
+        # with related 'book_author' entries
+        $c->stash->{object}->delete;
+
         # Redirect the user back to the list page with status msg as an arg
-        $c->response->redirect($c->uri_for('/books/list', 
+        $c->response->redirect($c->uri_for($self->action_for('list'),
             {status_msg => "Book deleted."}));
     }
 
 This modification simply leverages the ability of C<uri_for> to include
-an arbitrary number of name/value pairs in a hash reference.  Next, we 
-need to update C<root/src/wrapper> to handle C<status_msg> as a 
+an arbitrary number of name/value pairs in a hash reference.  Next, we
+need to update C<root/src/wrapper.tt2> to handle C<status_msg> as a
 query parameter:
 
     ...
@@ -552,27 +896,398 @@ query parameter:
     </div><!-- end content -->
     ...
 
-Although the sample above only shows the C<content> div, leave the 
+Although the sample above only shows the C<content> div, leave the
 rest of the file intact -- the only change we made to the C<wrapper.tt2>
-was to add "C<|| c.request.params.status_msg>" to the 
+was to add "C<|| c.request.params.status_msg>" to the
 C<E<lt>span class="message"E<gt>> line.
 
 
 =head2 Try the Delete and Redirect With Query Param Logic
 
-Restart the development server and point your browser to 
-L<http://localhost:3000/books/list>.  Then delete the remaining copy 
-of "TCPIP_Illustrated_Vol-2".  The green "Book deleted" status message
+Restart the development server and point your browser to
+L<http://localhost:3000/books/list> (you should now be able to safely
+hit "refresh" in your browser).  Then delete the remaining copy of
+"TCPIP_Illustrated_Vol-2".  The green "Book deleted" status message
 should return.
 
-B<NOTE:> Although this did present an opportunity to show a handy
-capability of C<uri_for>, it would be much better to use Catalyst's
-C<flash> feature in this situation.  Although the technique here is 
-less dangerous than leaving the delete URL in the client's browser, 
-we have still exposed the status message to the user.  With C<flash>, 
-this message returns to its rightful place as a service-side 
-mechanism (we will migrate this code to C<flash> in the next part 
-of the tutorial).
+B<NOTE:> Another popular method for maintaining server-side
+information across a redirect is to use the C<flash> technique we
+discuss in the next chapter of the tutorial,
+L<Authentication|Catalyst::Manual::Tutorial::Authentication>. While
+C<flash> is a "slicker" mechanism in that it's all handled by the
+server and doesn't "pollute" your URLs, B<it is important to note that
+C<flash> can lead to situations where the wrong information shows up
+in the wrong browser window if the user has multiple windows or
+browser tabs open>.  For example, Window A causes something to be
+placed in the stash, but before that window performs a redirect,
+Window B makes a request to the server and gets the status information
+that should really go to Window A.  For this reason, you may wish
+to use the "query param" technique shown here in your applications.
+
+
+=head1 EXPLORING THE POWER OF DBIC
+
+In this section we will explore some additional capabilities offered
+by DBIx::Class.  Although these features have relatively little to do
+with Catalyst per se, you will almost certainly want to take advantage
+of them in your applications.
+
+
+=head2 Add Datetime Columns to Our Existing Books Table
+
+Let's add two columns to our existing C<books> table to track when
+each book was added and when each book is updated:
+
+    $ sqlite3 myapp.db
+    sqlite> ALTER TABLE book ADD created INTEGER;
+    sqlite> ALTER TABLE book ADD updated INTEGER;
+    sqlite> UPDATE book SET created = DATETIME('NOW'), updated = DATETIME('NOW');
+    sqlite> SELECT * FROM book;
+    1|CCSP SNRS Exam Certification Guide|5|2009-03-08 16:26:35|2009-03-08 16:26:35
+    2|TCP/IP Illustrated, Volume 1|5|2009-03-08 16:26:35|2009-03-08 16:26:35
+    3|Internetworking with TCP/IP Vol.1|4|2009-03-08 16:26:35|2009-03-08 16:26:35
+    4|Perl Cookbook|5|2009-03-08 16:26:35|2009-03-08 16:26:35
+    5|Designing with Web Standards|5|2009-03-08 16:26:35|2009-03-08 16:26:35
+    9|TCP/IP Illustrated, Vol 3|5|2009-03-08 16:26:35|2009-03-08 16:26:35
+    sqlite> .quit
+    $
+
+This will modify the C<books> table to include the two new fields
+and populate those fields with the current time.
+
+
+=head2 Update DBIx::Class to Automatically Handle the Datetime Columns
+
+Next, we should re-run the DBIC helper to update the Result Classes
+with the new fields:
+
+    $ script/myapp_create.pl model DB DBIC::Schema MyApp::Schema \
+        create=static components=TimeStamp dbi:SQLite:myapp.db
+     exists "/root/dev/MyApp/script/../lib/MyApp/Model"
+     exists "/root/dev/MyApp/script/../t"
+    Dumping manual schema for MyApp::Schema to directory /root/dev/MyApp/script/../lib ...
+    Schema dump completed.
+     exists "/root/dev/MyApp/script/../lib/MyApp/Model/DB.pm"
+
+Notice that we modified our use of the helper slightly: we told
+it to include the L<DBIx::Class::TimeStamp|DBIx::Class::TimeStamp>
+in the C<load_components> line of the Result Classes.
+
+If you open C<lib/MyApp/Schema/Result/Book.pm> in your editor you
+should see that the C<created> and C<updated> fields are now included
+in the call to C<add_columns()>, but our relationship information below
+the "C<# DO NOT MODIFY...>" line was automatically preserved.
+
+While we have this file open, let's update it with some additional
+information to have DBIC automatically handle the updating of these
+two fields for us.  Insert the following code at the bottom of the
+file (it B<must> be B<below> the "C<# DO NOT MODIFY...>" line and
+B<above> the C<1;> on the last line):
+
+    #
+    # Enable automatic date handling
+    #
+    __PACKAGE__->add_columns(
+        "created",
+        { data_type => 'datetime', set_on_create => 1 },
+        "updated",
+        { data_type => 'datetime', set_on_create => 1, set_on_update => 1 },
+    );
+
+This will override the definition for these fields that Schema::Loader 
+placed at the top of the file.  The C<set_on_create> and 
+C<set_on_update> options will cause DBIx::Class to automatically 
+update the timestamps in these columns whenever a row is created or 
+modified.
+
+To test this out, restart the development server using the
+C<DBIC_TRACE=1> option:
+
+    DBIC_TRACE=1 script/myapp_server.pl
+
+Then enter the following URL into your web browser:
+
+    http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4
+
+You should get the same "Book Created" screen we saw above.  However,
+if you now use the sqlite3 command-line tool to dump the C<books> table,
+you will see that the new book we added has an appropriate date and
+time entered for it (see the last line in the listing below):
+
+    sqlite3 myapp.db "select * from book"
+    1|CCSP SNRS Exam Certification Guide|5|2009-03-08 16:26:35|2009-03-08 16:26:35
+    2|TCP/IP Illustrated, Volume 1|5|2009-03-08 16:26:35|2009-03-08 16:26:35
+    3|Internetworking with TCP/IP Vol.1|4|2009-03-08 16:26:35|2009-03-08 16:26:35
+    4|Perl Cookbook|5|2009-03-08 16:26:35|2009-03-08 16:26:35
+    5|Designing with Web Standards|5|2009-03-08 16:26:35|2009-03-08 16:26:35
+    9|TCP/IP Illustrated, Vol 3|5|2009-03-08 16:26:35|2009-03-08 16:26:35
+    10|TCPIP_Illustrated_Vol-2|5|2009-03-08 16:29:08|2009-03-08 16:29:08
+
+Notice in the debug log that the SQL DBIC generated has changed to
+incorporate the datetime logic:
+
+    INSERT INTO book (created, rating, title, updated) VALUES (?, ?, ?, ?):
+    '2009-03-08 16:29:08', '5', 'TCPIP_Illustrated_Vol-2', '2009-03-08 16:29:08'
+    INSERT INTO book_author (author_id, book_id) VALUES (?, ?): '4', '10'
+
+
+=head2 Create a ResultSet Class
+
+An often overlooked but extremely powerful features of DBIC is that it
+allows you to supply your own subclasses of C<DBIx::Class::ResultSet>.
+It allows you to pull complex and unsightly "query code" out of your
+controllers and encapsulate it in a method of your ResultSet Class.
+These "canned queries" in your ResultSet Class can then be invoked
+via a single call, resulting in much cleaner and easier to read
+controller code.
+
+To illustrate the concept with a fairly simple example, let's create a
+method that returns books added in the last 10 minutes.  Start by
+making a directory where DBIx::Class will look for our ResultSet Class:
+
+    mkdir lib/MyApp/Schema/ResultSet
+
+Then open C<lib/MyApp/Schema/ResultSet/Book.pm> and enter the following:
+
+    package MyApp::Schema::ResultSet::Book;
+
+    use strict;
+    use warnings;
+    use base 'DBIx::Class::ResultSet';
+
+    =head2 created_after
+
+    A predefined search for recently added books
+
+    =cut
+
+    sub created_after {
+        my ($self, $datetime) = @_;
+
+        my $date_str = $self->_source_handle->schema->storage
+                              ->datetime_parser->format_datetime($datetime);
+
+        return $self->search({
+            created => { '>' => $date_str }
+        });
+    }
+
+    1;
+
+Then we need to tell the Result Class to to treat this as a ResultSet
+Class.  Open C<lib/MyApp/Schema/Result/Book.pm> and add the following
+above the "C<1;>" at the bottom of the file:
+
+    #
+    # Set ResultSet Class
+    #
+    __PACKAGE__->resultset_class('MyApp::Schema::ResultSet::Book');
+
+Then add the following method to the C<lib/MyApp/Controller/Books.pm>:
+
+    =head2 list_recent
+
+    List recently created books
+
+    =cut
+
+    sub list_recent :Chained('base') :PathPart('list_recent') :Args(1) {
+        my ($self, $c, $mins) = @_;
+
+        # Retrieve all of the book records as book model objects and store in the
+        # stash where they can be accessed by the TT template, but only
+        # retrieve books created within the last $min number of minutes
+        $c->stash->{books} = [$c->model('DB::Book')
+                                ->created_after(DateTime->now->subtract(minutes => $mins))];
+
+        # Set the TT template to use.  You will almost always want to do this
+        # in your action methods (action methods respond to user input in
+        # your controllers).
+        $c->stash->{template} = 'books/list.tt2';
+    }
+
+Now start the development server with C<DBIC_TRACE=1> and try
+different values for the minutes argument (the final number value) for
+the URL C<http://localhost:3000/books/list_recent/10>.  For example,
+this would list all books added in the last fifteen minutes:
+
+    http://localhost:3000/books/list_recent/15
+
+Depending on how recently you added books, you might want to
+try a higher or lower value.
+
+
+=head2 Chaining ResultSets
+
+One of the most helpful and powerful features in DBIx::Class is that 
+it allows you to "chain together" a series of queries (note that this 
+has nothing to do with the "Chained Dispatch" for Catalyst that we 
+were discussing above).  Because each ResultSet returns another 
+ResultSet, you can take an initial query and immediately feed that 
+into a second query (and so on for as many queries you need). Note 
+that no matter how many ResultSets you chain together, the database 
+itself will not be hit until you use a method that attempts to access 
+the data. And, because this technique carries over to the ResultSet 
+Class feature we implemented in the previous section for our "canned 
+search", we can combine the two capabilities.  For example, let's add 
+an action to our C<Books> controller that lists books that are both 
+recent I<and> have "TCP" in the title.  Open up 
+C<lib/MyApp/Controller/Books.pm> and add the following method:
+
+    =head2 list_recent_tcp
+
+    List recently created books
+
+    =cut
+
+    sub list_recent_tcp :Chained('base') :PathPart('list_recent_tcp') :Args(1) {
+        my ($self, $c, $mins) = @_;
+
+        # Retrieve all of the book records as book model objects and store in the
+        # stash where they can be accessed by the TT template, but only
+        # retrieve books created within the last $min number of minutes
+        # AND that have 'TCP' in the title
+        $c->stash->{books} = [$c->model('DB::Book')
+                                ->created_after(DateTime->now->subtract(minutes => $mins))
+                                ->search({title => {'like', '%TCP%'}})
+                             ];
+
+        # Set the TT template to use.  You will almost always want to do this
+        # in your action methods (action methods respond to user input in
+        # your controllers).
+        $c->stash->{template} = 'books/list.tt2';
+    }
+
+To try this out, restart the development server with:
+
+    DBIC_TRACE=1 script/myapp_server.pl
+
+And enter the following URL into your browser:
+
+    http://localhost:3000/books/list_recent_tcp/100
+
+And you should get a list of books added in the last 100 minutes that
+contain the string "TCP" in the title.  However, if you look at all
+books within the last 100 minutes, you should get a longer list
+(again, you might have to adjust the number of minutes depending on
+how recently you added books to your database):
+
+    http://localhost:3000/books/list_recent/100
+
+Take a look at the DBIC_TRACE output in the development server log for
+the first URL and you should see something similar to the following:
+
+    SELECT me.id, me.title, me.rating, me.created, me.updated FROM book me
+    WHERE ( ( ( title LIKE ? ) AND ( created > ? ) ) ): '%TCP%', '2009-03-08 14:52:54'
+
+However, let's not pollute our controller code with this raw "TCP"
+query -- it would be cleaner to encapsulate that code in a method on
+our ResultSet Class.  To do this, open
+C<lib/MyApp/Schema/ResultSet/Book.pm> and add the following method:
+
+    =head2 title_like
+
+    A predefined search for books with a 'LIKE' search in the string
+
+    =cut
+
+    sub title_like {
+        my ($self, $title_str) = @_;
+
+        return $self->search({
+            title => { 'like' => "%$title_str%" }
+        });
+    }
+
+We defined the search string as C<$title_str> to make the method more
+flexible.  Now update the C<list_recent_tcp> method in
+C<lib/MyApp/Controller/Books.pm> to match the following (we have
+replaced the C<-E<gt>search> line with the C<-E<gt>title_like> line
+shown here -- the rest of the method should be the same):
+
+    =head2 list_recent_tcp
+
+    List recently created books
+
+    =cut
+
+    sub list_recent_tcp :Chained('base') :PathPart('list_recent_tcp') :Args(1) {
+        my ($self, $c, $mins) = @_;
+
+        # Retrieve all of the book records as book model objects and store in the
+        # stash where they can be accessed by the TT template, but only
+        # retrieve books created within the last $min number of minutes
+        # AND that have 'TCP' in the title
+        $c->stash->{books} = [$c->model('DB::Book')
+                                ->created_after(DateTime->now->subtract(minutes => $mins))
+                                ->title_like('TCP')
+                             ];
+
+        # Set the TT template to use.  You will almost always want to do this
+        # in your action methods (action methods respond to user input in
+        # your controllers).
+        $c->stash->{template} = 'books/list.tt2';
+    }
+
+Then restart the development server and try out the C<list_recent_tcp>
+and C<list_recent> URL as we did above.  It should work just the same,
+but our code is obviously cleaner and more modular, while also being
+more flexible at the same time.
+
+
+=head2 Adding Methods to Result Classes
+
+In the previous two sections we saw a good example of how we could use 
+DBIx::Class ResultSet Classes to clean up our code for an entire query 
+(for example, our "canned searches" that filtered the entire query). 
+We can do a similar improvement when working with individual rows as 
+well.  Whereas the ResultSet construct is used in DBIC to correspond 
+to an entire query, the Result Class construct is used to represent a 
+row. Therefore, we can add row-specific "helper methods" to our Result 
+Classes stored in C<lib/MyApp/Schema/Result/>. For example, open 
+C<lib/MyApp/Schema/Result/Author.pm> and add the following method (as 
+always, it must be above the closing "C<1;>"):
+
+    #
+    # Helper methods
+    #
+    sub full_name {
+        my ($self) = @_;
+
+        return $self->first_name . ' ' . $self->last_name;
+    }
+
+This will allow us to conveniently retrieve both the first and last
+name for an author in one shot.  Now open C<root/src/books/list.tt2>
+and change the definition of C<tt_authors> from this:
+
+    ...
+      [% tt_authors = [ ];
+         tt_authors.push(author.last_name) FOREACH author = book.author %]
+    ...
+
+to:
+
+    ...
+      [% tt_authors = [ ];
+         tt_authors.push(author.full_name) FOREACH author = book.author %]
+    ...
+
+(Only C<author.last_name> was changed to C<author.full_name> -- the
+rest of the file should remain the same.)
+
+Now restart the development server and go to the standard book list
+URL:
+
+    http://localhost:3000/books/list
+
+The "Author(s)" column will now contain both the first and last name.
+And, because the concatenation logic was encapsulated inside our
+Result Class, it keeps the code inside our .tt template nice and clean
+(remember, we want the templates to be as close to pure HTML markup as
+possible). Obviously, this capability becomes even more useful as you
+use to to remove even more complicated row-specific logic from your
+templates!
 
 
 =head1 AUTHOR
@@ -585,4 +1300,3 @@ L<http://dev.catalyst.perl.org/repos/Catalyst/Catalyst-Manual/5.70/trunk/lib/Cat
 
 Copyright 2006-2008, Kennedy Clark, under Creative Commons License
 (L<http://creativecommons.org/licenses/by-sa/3.0/us/>).
-