X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=catagits%2FCatalyst-Manual.git;a=blobdiff_plain;f=lib%2FCatalyst%2FManual%2FTutorial%2FBasicCRUD.pod;h=463e8d5acd458bdc4f8bc00d859c420fdc731d8f;hp=1f191b1c15ee8f92c3c9ec9a7c772f65955b528f;hb=3b1fa91be1d89d2297aa9e8e83462344d9cd9820;hpb=d04961970a25ec3dc831f89be5cd6e27fdec884a diff --git a/lib/Catalyst/Manual/Tutorial/BasicCRUD.pod b/lib/Catalyst/Manual/Tutorial/BasicCRUD.pod index 1f191b1..463e8d5 100644 --- a/lib/Catalyst/Manual/Tutorial/BasicCRUD.pod +++ b/lib/Catalyst/Manual/Tutorial/BasicCRUD.pod @@ -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 for the Catalyst tutorial. +This is B for the Catalyst tutorial. L @@ -56,24 +56,32 @@ L =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 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. - -You can checkout the source code for this example from the catalyst -subversion repository as per the instructions in -L +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 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, +L, and +L. + +You can check out the source code for this example from the Catalyst +Subversion repository as per the instructions in +L. =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). @@ -83,42 +91,34 @@ based submission in the sections that follow). Edit C 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 "///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'; } @@ -127,12 +127,12 @@ Notice that Catalyst takes "extra slash-separated information" from the URL and passes it as arguments in C<@_>. The C action then uses a simple call to the DBIC C method to add the requested information to the database (with a separate call to -C to update the join table). As do virtually all +C 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 Action: +=head2 Include a Template for the 'url_create' Action: Edit C and then enter: @@ -140,54 +140,53 @@ Edit C 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. -%]

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 %].

- + [% # Provide a link back to the list page -%] [% # 'uri_for()' builds a full URI; e.g., 'http://localhost:3000/books/list' -%] -

Return to list

- +

Return to list

+ [% # Try out the TT Dumper (for development only!) -%]
     Dump of the 'book' variable:
     [% Dumper.dump(book) %]
     
-The TT C 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 -"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 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 "pretty printing" of objects and +variables. Other than that, the rest of the code should be familiar +from the examples in Chapter 3. -B As mentioned earlier, the C view -class created by TTSite redefines the name used to access the Catalyst -context object in TT templates from the usual C to C. -=head2 Try the C Feature +=head2 Try the 'url_create' Feature If the application is still running from before, use C to kill it. Then restart the server: - $ script/myapp_server.pl + $ DBIC_TRACE=1 script/myapp_server.pl Note that new path for C appears in the startup debug output. @@ -203,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 statements are obviously adding the book and linking it to the existing record for Richard Stevens. The C Rating: @@ -278,40 +497,41 @@ Open C in your editor and enter: Note that we have specified the target of the form data as C, the method created in the section that follows. + =head2 Add a Method to Process Form Values and Update Database Edit C 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'; } @@ -324,38 +544,53 @@ 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 and enter "TCP/IP Illustrated, Vol 3" for the title, a rating of 5, and an -author ID of 4. You should then be forwarded to the same +author ID of 4. You should then see the output of the same C template seen in earlier examples. Finally, click "Return to list" to view the full list of books. B 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 and update it to the following (two +Edit C and update it to match the following (two sections have changed: 1) the additional 'Links' 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' -%] - + [% # Display each book in a table row %] @@ -366,21 +601,22 @@ and 2) the four lines for the Delete link near the bottom). [% END -%] @@ -388,39 +624,127 @@ 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 "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 than +we have seen before. Here we use +C<$c-Econtroller-Eaction_for> to automatically generate a URI +appropriate for that action based on the method we want to link to +while inserting the C 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: + +=over 4 + +=item * + +If you are referring to a method in the current controller, you can +use C<$self-Eaction_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, as in +C<$c-Econtroller('_controller_name_')-Eaction_for('_method_name_')>. + +=back + +B In practice you should B 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 +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 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 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 that don't operate on an existing book can chain +directly off base. + +To add the C method, edit C +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 will automatically +have the appropriate book waiting for it in +C<$c-Estash-E{object}>. + +Also note that we are using a different technique for setting +C<$c-Estash>. 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-Estash(name =E 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 in your editor and add the following method: - =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."; - + # 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 method. However, it also removes the corresponding entry from the -C table. Note that C was used instead of -C: whereas C also removes the join table entries in -C, C does not (only use C if you -really need the cascading deletes... otherwise you are wasting resources). +C 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 to display a @@ -438,24 +762,50 @@ equivalent. If the application is still running from before, use C to kill it. Then restart the server: - $ script/myapp_server.pl + $ DBIC_TRACE=1 script/myapp_server.pl + +The C 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 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-Eforward('list'))> or C<$c-Edetach('list'))> that perform @@ -465,100 +815,479 @@ 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 and edit the existing +open C and edit the existing C 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 and delete the first copy of -"TCPIP_Illustrated_Vol-2". The URL in your browser should return to -the L URL, so that is an -improvement, but notice that I. Because the stash is reset on every request (and a -redirect involves a second request), the C is cleared -before it can be displayed. +Restart the development server and point your browser to +L (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 URL, so that is an +improvement, but notice that I. Because the stash is reset on every request (and a redirect +involves a second request), the C is cleared before it can +be displayed. -=head2 Using C to Pass Query Parameters +=head2 Using 'uri_for' to Pass Query Parameters -There are several ways to pass information across a redirect. -In general, the best option is to use the C 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 and update the existing -C method to match the following: +There are several ways to pass information across a redirect. One +option is to use the C 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 and update the existing C +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 to include -an arbitrary number of name/value pairs in a hash reference. Next, we -need to update C to handle C as a +an arbitrary number of name/value pairs in a hash reference. Next, we +need to update C to handle C as a query parameter: - - + ...
- [% status_msg || Catalyst.request.params.status_msg %] - [% error_msg %] - [% content %] -
- - + [%# Status and error messages %] + [% status_msg || c.request.params.status_msg %] + [% error_msg %] + [%# This is where TT will stick all of your template's contents. -%] + [% content %] + + ... + +Although the sample above only shows the C div, leave the +rest of the file intact -- the only change we made to the C +was to add "C<|| c.request.params.status_msg>" to the +Cspan class="message"E> line. =head2 Try the Delete and Redirect With Query Param Logic -Restart the development server and point your browser to -L. 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 (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 Although this did present an opportunity to show a handy -capability of C, it would be much better to use Catalyst's -C 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, -this message returns to its rightful place as a service-side -mechanism (we will migrate this code to C in the next part -of the tutorial). +B Another popular method for maintaining server-side +information across a redirect is to use the C technique we +discuss in the next chapter of the tutorial, +L. While +C is a "slicker" mechanism in that it's all handled by the +server and doesn't "pollute" your URLs, B 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 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 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 +in the C line of the Result Classes. + +If you open C in your editor you +should see that the C and C fields are now included +in the call to C, 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 be B the "C<# DO NOT MODIFY...>" line and +B 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 and +C 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 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 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. +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 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 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: + + =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 and try +different values for the minutes argument (the final number value) for +the URL C. 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 controller that lists books that are both +recent I have "TCP" in the title. Open up +C 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 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 method in +C to match the following (we have +replaced the C<-Esearch> line with the C<-Etitle_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 +and C 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. For example, open +C 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 +and change the definition of C 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 was changed to C -- 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 @@ -567,8 +1296,7 @@ Kennedy Clark, C Please report any errors, issues or suggestions to the author. The most recent version of the Catalyst Tutorial can be found at -L. - -Copyright 2006, Kennedy Clark, under Creative Commons License -(L). +L. +Copyright 2006-2008, Kennedy Clark, under Creative Commons License +(L).
TitleRatingAuthor(s)Links
[% # 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 %] [% # Add a link to delete a book %] - Delete + Delete