+++ /dev/null
-=head1 NAME
-
-Catalyst::Manual::Tutorial::BasicCRUD - Catalyst Tutorial - Chapter 4: Basic CRUD
-
-
-=head1 OVERVIEW
-
-This is B<Chapter 4 of 10> for the Catalyst tutorial.
-
-L<Tutorial Overview|Catalyst::Manual::Tutorial>
-
-=over 4
-
-=item 1
-
-L<Introduction|Catalyst::Manual::Tutorial::Intro>
-
-=item 2
-
-L<Catalyst Basics|Catalyst::Manual::Tutorial::CatalystBasics>
-
-=item 3
-
-L<More Catalyst Basics|Catalyst::Manual::Tutorial::MoreCatalystBasics>
-
-=item 4
-
-B<Basic CRUD>
-
-=item 5
-
-L<Authentication|Catalyst::Manual::Tutorial::Authentication>
-
-=item 6
-
-L<Authorization|Catalyst::Manual::Tutorial::Authorization>
-
-=item 7
-
-L<Debugging|Catalyst::Manual::Tutorial::Debugging>
-
-=item 8
-
-L<Testing|Catalyst::Manual::Tutorial::Testing>
-
-=item 9
-
-L<Advanced CRUD|Catalyst::Manual::Tutorial::AdvancedCRUD>
-
-=item 10
-
-L<Appendices|Catalyst::Manual::Tutorial::Appendices>
-
-=back
-
-
-=head1 DESCRIPTION
-
-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::CRUD|CatalystX::CRUD>, and
-L<CatalystX::CRUD::YUI|CatalystX::CRUD::YUI>.
-
-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
-arguments" feature of Catalyst (we will employ the more common form-
-based submission in the sections that follow).
-
-
-=head2 Include a Create Action in the Books Controller
-
-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/"
- # into @_
- my ($self, $c, $title, $rating, $author_id) = @_;
-
- # 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::Book')->create({
- title => $title,
- rating => $rating
- });
-
- # Add a record to the join table for this book, mapping to
- # appropriate author
- $book->add_to_book_author({author_id => $author_id});
- # Note: Above is a shortcut for this:
- # $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;
-
- # Set the TT template to use
- $c->stash->{template} = 'books/create_done.tt2';
- }
-
-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_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 'url_create' Action:
-
-Edit C<root/src/books/create_done.tt2> and then enter:
-
- [% # Use the TT Dumper plugin to Data::Dumper variables to the browser -%]
- [% # 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 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.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;
- 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 Chapter 3.
-
-
-=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:
-
- $ DBIC_TRACE=1 script/myapp_server.pl
-
-Note that new path for C</books/url_create> appears in the startup debug
-output.
-
-B<TIP>: You can use C<script/myapp_server.pl -r> to have the development
-server auto-detect changed files and reload itself (if your browser acts
-odd, you should also try throwing in a C<-k>). If you make changes to
-the TT templates only, you do not need to reload the development server
-(only changes to "compiled code" such as Controller and Model C<.pm>
-files require a reload).
-
-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
-'Stevens' with a rating of 5." along with a dump of the new book model
-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 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). 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()>
-
-=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
-
-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
-
-Although the C<url_create> action in the previous step does begin to
-reveal the power and flexibility of both Catalyst and DBIC, it's
-obviously not a very realistic example of how users should be expected
-to enter data. This section begins to address that concern.
-
-
-=head2 Add Method to Display The Form
-
-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 :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 form to create a book.
-
-
-=head2 Add a Template for the 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>
- <tr><td>Rating:</td><td><input type="text" name="rating"></td></tr>
- <tr><td>Author ID:</td><td><input type="text" name="author_id"></td></tr>
- </table>
- <input type="submit" name="Submit" value="Submit">
- </form>
-
-Note that we have specified the target of the form data as
-C<form_create_do>, the method created in the section that follows.
-
-
-=head2 Add a Method to Process Form Values and Update Database
-
-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 :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::Book')->create({
- title => $title,
- rating => $rating,
- });
- # Handle relationship with author
- $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
- $Data::Dumper::Useperl = 1;
-
- # Set the TT template to use
- $c->stash->{template} = 'books/create_done.tt2';
- }
-
-
-=head2 Test Out The Form
-
-If the application is still running from before, use C<Ctrl-C> to kill
-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
-C<create_done.tt2> template seen in earlier examples. Finally, click
-"Return to list" to view the full list of books.
-
-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
-Chapter 9.
-
-
-=head1 A SIMPLE DELETE FEATURE
-
-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 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):
-
- [% # 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 -%]
- [% 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 %]
- [% FOREACH book IN books -%]
- <tr>
- <td>[% book.title %]</td>
- <td>[% book.rating %]</td>
- <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 doesn't return -%]
- [% # a value, so nothing will be printed here. But, if you have something -%]
- [% # 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.author %]
- [% # Now use a TT 'virtual method' to display the author count in parens -%]
- [% # 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(', ') | html %]
- </td>
- <td>
- [% # Add a link to delete a book %]
- <a href="[% c.uri_for(c.controller.action_for('delete'), [book.id]) %]">Delete</a>
- </td>
- </tr>
- [% END -%]
- </table>
-
-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; 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
-
-Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
-following method:
-
- =head2 delete
-
- Delete a book
-
- =cut
-
- 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 object saved by the C<object> method.
-However, it also removes the corresponding entry from the
-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
-notification to the user as the normal list view is rendered.
-
-The C<delete> action uses the context C<forward> method to return the
-user to the book list. The C<detach> method could have also been used.
-Whereas C<forward> I<returns> to the original action once it is
-completed, C<detach> does I<not> return. Other than that, the two are
-equivalent.
-
-
-=head2 Try the Delete Feature
-
-If the application is still running from before, use C<Ctrl-C> to kill
-it. Then restart the server:
-
- $ 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. 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
-prior step -- it is still referencing the delete action:
-
- 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 (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
-a server-side alteration in the flow of processing, a redirect is a
-client-side mechanism that causes the browser to issue an entirely
-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
-C<sub delete> method to match:
-
- =head2 delete
-
- Delete a book
-
- =cut
-
- 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. 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> (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
-
-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:
-
- =head2 delete
-
- Delete a book
-
- =cut
-
- 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($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.tt2> to handle C<status_msg> as a
-query parameter:
-
- ...
- <div id="content">
- [%# Status and error messages %]
- <span class="message">[% status_msg || c.request.params.status_msg %]</span>
- <span class="error">[% error_msg %]</span>
- [%# This is where TT will stick all of your template's contents. -%]
- [% content %]
- </div><!-- end content -->
- ...
-
-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
-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> (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:> 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
-
-Kennedy Clark, C<hkclark@gmail.com>
-
-Please report any errors, issues or suggestions to the author. The
-most recent version of the Catalyst Tutorial can be found at
-L<http://dev.catalyst.perl.org/repos/Catalyst/Catalyst-Manual/5.70/trunk/lib/Catalyst/Manual/Tutorial/>.
-
-Copyright 2006-2008, Kennedy Clark, under Creative Commons License
-(L<http://creativecommons.org/licenses/by-sa/3.0/us/>).