- •Contents
- •Preface to the Second Edition
- •Introduction
- •Rails Is Agile
- •Finding Your Way Around
- •Acknowledgments
- •Getting Started
- •The Architecture of Rails Applications
- •Models, Views, and Controllers
- •Active Record: Rails Model Support
- •Action Pack: The View and Controller
- •Installing Rails
- •Your Shopping List
- •Installing on Windows
- •Installing on Mac OS X
- •Installing on Linux
- •Development Environments
- •Rails and Databases
- •Rails and ISPs
- •Creating a New Application
- •Hello, Rails!
- •Linking Pages Together
- •What We Just Did
- •Building an Application
- •The Depot Application
- •Incremental Development
- •What Depot Does
- •Task A: Product Maintenance
- •Iteration A1: Get Something Running
- •Iteration A2: Add a Missing Column
- •Iteration A3: Validate!
- •Iteration A4: Prettier Listings
- •Task B: Catalog Display
- •Iteration B1: Create the Catalog Listing
- •Iteration B4: Linking to the Cart
- •Task C: Cart Creation
- •Sessions
- •Iteration C1: Creating a Cart
- •Iteration C2: A Smarter Cart
- •Iteration C3: Handling Errors
- •Iteration C4: Finishing the Cart
- •Task D: Add a Dash of AJAX
- •Iteration D1: Moving the Cart
- •Iteration D3: Highlighting Changes
- •Iteration D4: Hide an Empty Cart
- •Iteration D5: Degrading If Javascript Is Disabled
- •What We Just Did
- •Task E: Check Out!
- •Iteration E1: Capturing an Order
- •Task F: Administration
- •Iteration F1: Adding Users
- •Iteration F2: Logging In
- •Iteration F3: Limiting Access
- •Iteration F4: A Sidebar, More Administration
- •Task G: One Last Wafer-Thin Change
- •Generating the XML Feed
- •Finishing Up
- •Task T: Testing
- •Tests Baked Right In
- •Unit Testing of Models
- •Functional Testing of Controllers
- •Integration Testing of Applications
- •Performance Testing
- •Using Mock Objects
- •The Rails Framework
- •Rails in Depth
- •Directory Structure
- •Naming Conventions
- •Logging in Rails
- •Debugging Hints
- •Active Support
- •Generally Available Extensions
- •Enumerations and Arrays
- •String Extensions
- •Extensions to Numbers
- •Time and Date Extensions
- •An Extension to Ruby Symbols
- •with_options
- •Unicode Support
- •Migrations
- •Creating and Running Migrations
- •Anatomy of a Migration
- •Managing Tables
- •Data Migrations
- •Advanced Migrations
- •When Migrations Go Bad
- •Schema Manipulation Outside Migrations
- •Managing Migrations
- •Tables and Classes
- •Columns and Attributes
- •Primary Keys and IDs
- •Connecting to the Database
- •Aggregation and Structured Data
- •Miscellany
- •Creating Foreign Keys
- •Specifying Relationships in Models
- •belongs_to and has_xxx Declarations
- •Joining to Multiple Tables
- •Acts As
- •When Things Get Saved
- •Preloading Child Rows
- •Counters
- •Validation
- •Callbacks
- •Advanced Attributes
- •Transactions
- •Action Controller: Routing and URLs
- •The Basics
- •Routing Requests
- •Action Controller and Rails
- •Action Methods
- •Cookies and Sessions
- •Caching, Part One
- •The Problem with GET Requests
- •Action View
- •Templates
- •Using Helpers
- •How Forms Work
- •Forms That Wrap Model Objects
- •Custom Form Builders
- •Working with Nonmodel Fields
- •Uploading Files to Rails Applications
- •Layouts and Components
- •Caching, Part Two
- •Adding New Templating Systems
- •Prototype
- •Script.aculo.us
- •RJS Templates
- •Conclusion
- •Action Mailer
- •Web Services on Rails
- •Dispatching Modes
- •Using Alternate Dispatching
- •Method Invocation Interception
- •Testing Web Services
- •Protocol Clients
- •Secure and Deploy Your Application
- •Securing Your Rails Application
- •SQL Injection
- •Creating Records Directly from Form Parameters
- •Avoid Session Fixation Attacks
- •File Uploads
- •Use SSL to Transmit Sensitive Information
- •Knowing That It Works
- •Deployment and Production
- •Starting Early
- •How a Production Server Works
- •Repeatable Deployments with Capistrano
- •Setting Up a Deployment Environment
- •Checking Up on a Deployed Application
- •Production Application Chores
- •Moving On to Launch and Beyond
- •Appendices
- •Introduction to Ruby
- •Classes
- •Source Code
- •Resources
- •Index
- •Symbols
CACHING, PAR T TWO 513
Here’s how this controller can use a filter to set the cart into the context of each action.
class ShopController < ActionController::Base before_filter :set_cart
def index
@products = Product.find(:all) end
def buy
@cart << Product.find(1) redirect_to :action => "index"
end
private
def set_cart
@cart = Cart.find(session[:cart_id]) end
end
And here’s the index.rhtml view:
<h1>My Magic Shop!</h1>
<div id="products" >
<%= render :partial => "product", :collection => @products %>
</div>
<div id="cart">
<%= render :partial => "cart" %>
</div>
This shows how the cart is used in the act of buying, solely through the controller, and also how the index view can rely on the @cart being available for partial showing. The great thing about separating partial and context is that you can manipulate one without the other. So the partial for the cart can be used with any kind of cart—perhaps for use in an administration interface that inspects active carts.
Components are scheduled to become a plugin with Rails 2.0. So if you’ve already built your application using components, you won’t be left out in the cold. But it should send a strong signal that components are not encouraged for everyday use.
22.10Caching, Part Two
We looked at the page caching support in Action Controller starting back on page 455. We said that Rails also allows you to cache parts of a page. This turns out to be remarkably useful in dynamic sites. Perhaps you customize the greeting and the sidebar on your blog application for each individual user.
Report erratum
CACHING, PAR T TWO 514
In this case you can’t use page caching, because the overall page is different for each user. But because the list of articles doesn’t change between users, you can use fragment caching—you construct the HTML that displays the articles just once and include it in customized pages delivered to individual users.
Just to illustrate fragment caching, let’s set up a pretend blog application. Here’s the controller. It sets up @dynamic_content, representing content that should change each time the page is viewed. For our fake blog, we use the current time as this content.
Download e1/views/app/controllers/blog_controller.rb
class BlogController < ApplicationController def list
@dynamic_content = Time.now.to_s end
end
Here’s our mock Article class. It simulates a model class that in normal circumstances would fetch articles from the database. We’ve arranged for the first article in our list to display the time at which it was created.
Download e1/views/app/models/article.rb
class Article attr_reader :body
def initialize(body) @body = body
end
def self.find_recent
[new("It is now #{Time.now.to_s}"), new("Today I had pizza"),
new("Yesterday I watched Spongebob"), new("Did nothing on Saturday") ]
end end
Now we’d like to set up a template that uses a cached version of the rendered articles but still updates the dynamic data. It turns out to be trivial.
Download e1/views/app/views/blog/list.rhtml
<%= @dynamic_content %> |
<!- Here's dynamic content. -> |
|
<% cache do %> |
<!- Here's the content we cache -> |
|
<ul> |
|
|
<% |
for article in Article.find_recent -%> |
|
|
<li><p><%= h(article.body) %></p></li> |
|
<% |
end -%> |
|
</ul> |
|
|
<% end |
%> |
<!- End of cached content -> |
<%= @dynamic_content %> |
<!- More dynamic content. -> |
Report erratum
CACHING, PAR T TWO 515
Refresh page
Figure 22.4: Refreshing a Page with Cached and Noncached Data
The magic is the cache method. All output generated in the block associated with this method will be cached. The next time this page is accessed, the dynamic content will still be rendered, but the stuff inside the block will come straight from the cache—it won’t be regenerated. We can see this if we bring up our skeletal application and hit Refresh after a few seconds, as shown in Figure 22.4. The times at the top and bottom of the page—the dynamic portion of our data—change on the refresh. However, the time in the center section remains the same: it is being served from the cache. (If you’re trying this at home and you see all three time strings change, chances are you’re running your application in development mode. Caching is enabled by default only in production mode. If you’re testing using WEBrick, the -e production option will do the trick.)
The key concept here is that the stuff that’s cached is the fragment generated in the view. If we’d constructed the article list in the controller and then passed that list to the view, the future access to the page would not have to rerender the list, but the database would still be accessed on every request. Moving the database request into the view means it won’t be called once the output is cached.
OK, you say, but that just broke the rule about putting application-level code into view templates. Can’t we avoid that somehow? We can, but it means making caching just a little less transparent than it would otherwise be. The trick is to have the action test for the presence of a cached fragment. If one exists, the action bypasses the expensive database operation, knowing that the fragment will be used.
Report erratum
CACHING, PAR T TWO 516
Download e1/views/app/controllers/blog1_controller.rb
class Blog1Controller < ApplicationController
def list
@dynamic_content = Time.now.to_s
unless read_fragment(:action => 'list') logger.info("Creating fragment") @articles = Article.find_recent
end end
end
The action uses the read_fragment method to see whether a fragment exists for this action. If not, it loads the list of articles from the (fake) database. The view then uses this list to create the fragment.
Download e1/views/app/views/blog1/list.rhtml
<%= @dynamic_content %> <!- Here's dynamic content. ->
<% cache do %> |
<!- Here's the content we cache -> |
|
<ul> |
|
|
<% |
for article in @articles -%> |
|
|
<li><p><%= h(article.body) %></p></li> |
|
<% |
end -%> |
|
</ul> |
|
|
<% end |
%> |
<!- End of the cached content -> |
<%= @dynamic_content |
%> <!- More dynamic content. -> |
Expiring Cached Fragments
Now that we have a cached version of the article list, our Rails application will to serve it whenever this page is referenced. If the articles are updated, however, the cached version will be out-of-date and should be expired. We do this with the expire_fragment method. By default, fragments are cached using the name of the controller and action that rendered the page (blog and list in our first case). To expire the fragment (for example, when the article list changes), the controller could call
Download e1/views/app/controllers/blog_controller.rb
expire_fragment(:controller => 'blog', :action => 'list')
Clearly, this naming scheme works only if there’s just one fragment on the page. Fortunately, if you need more, you can override the names associated with fragments by adding parameters (using url_for conventions) to the cache method.
Report erratum
CACHING, PAR T TWO 517
Download e1/views/app/views/blog2/list.rhtml
<% cache(:action => 'list', :part => 'articles') do %>
<ul>
<% for article in @articles -%> <li><p><%= h(article.body) %></p></li>
<% end -%>
</ul>
<% end %>
<% cache(:action => 'list', :part => 'counts') do %>
<p>
There are a total of <%= @article_count %> articles.
</p>
<% end %>
In this example two fragments are cached. The first is saved with the additional :part parameter set to articles, the second with it set to counts.
Within the controller, we can pass the same parameters to expire_fragment to delete particular fragments. For example, when we edit an article, we have to expire the article list, but the count is still valid. If instead we delete an article, we need to expire both fragments. The controller looks like this (we don’t have any code that actually does anything to the articles in it—just look at the caching).
Download e1/views/app/controllers/blog2_controller.rb
class Blog2Controller < ApplicationController
def list
@dynamic_content = Time.now.to_s @articles = Article.find_recent
@article_count |
= @articles.size |
|
end |
|
|
def |
edit |
|
# |
do the article editing |
expire_fragment(:action => 'list', :part => 'articles') redirect_to(:action => 'list')
end
def delete
# do the deleting
expire_fragment(:action => 'list', :part => 'articles') expire_fragment(:action => 'list', :part => 'counts') redirect_to(:action => 'list')
end end
The expire_fragment method can also take a single regular expression as a parameter, allowing us to expire all fragments whose names match.
expire_fragment(%r{/blog2/list.*})
Report erratum