Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Agile Web Development With Rails, 2nd Edition (2006).pdf
Скачиваний:
30
Добавлен:
17.08.2013
Размер:
6.23 Mб
Скачать

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