#90
Jan 27, 2008

Fragment Caching

Sometimes you only want to cache a section of a page instead of the entire page. Fragment caching is the answer as shown in this episode.
Download (20.5 MB, 6:54)
alternative download for iPod & Apple TV (11.4 MB, 6:54)

Resources

# products_controller.rb
cache_sweeper :product_sweeper, :only => [:create, :update, :destroy]

# models/product.rb
class Product < ActiveRecord::Base
  def self.find_recent
    find(:all, :order => 'released_at desc', :limit => 10)
  end
end

# config/environments/development.rb
config.action_controller.perform_caching = true

# config/environment.rb
config.load_paths << "#{RAILS_ROOT}/app/sweepers"

# sweepers/product_sweeper.rb
class ProductSweeper < ActionController::Caching::Sweeper
  observe Product
  
  def after_save(product)
    expire_cache(product)
  end
  
  def after_destroy(product)
    expire_cache(product)
  end
  
  def expire_cache(product)
    expire_fragment 'recent_products'
  end
end
<% cache 'recent_products' do %>
<div id="recent_products">
  <h2>Recent Products</h2>
  <ul>
  <% for product in Product.find_recent %>
    <li><%= link_to h(product.name), product %></li>
  <% end %>
  </ul>
</div>
<% end %>

25 comments

Dejan Dimic Jan 27, 2008 at 23:31

This subject is one of the most important if you plan to have a dynamic and responsive web site.

Most of programmers come to this point late in the development process.

As usual Ryan is right on the spot, so thanks once again.


Stijn Goris Jan 28, 2008 at 01:56

Many thanks since I was looking into this topic. Great Railscast!


heip Jan 28, 2008 at 02:58

What about:

controller:
unless read_fragment("recent_products")
@product = Product.find_recent
end


Michel Jan 28, 2008 at 04:26

Link is down for me:
http://media.railscasts.com/videos/090_fragment_caching.mov


Ryan Bates Jan 28, 2008 at 07:24

@heip, that would work too. I still prefer to place the find in the view, but it's subjective.

@Michel, it's working for me. Is it still down for you?


David Parker Jan 28, 2008 at 08:10

Ryan, great job! Definitely something I knew of, but know I know how. Thanks.


Bryce Jan 28, 2008 at 08:32

Ryan: Thanks for another GREAT 'cast. You inspire me to write great code.

Please help to understand: I thought database calls were meant to be in the controller. I understand moving logic to the model, but I thought it was more appropriate to keep calls out of the view and in the controller. Am I wrong here or is best to keep calls in the controller unless the fragment is cached?

Thanks again for your great screen casts.
Bryce


chris Jan 28, 2008 at 09:50

Another great screencast.

I still have a question, though. Is caching something I should test? How would I do this?


Jon Buda Jan 28, 2008 at 12:32

You hit another one out of the park with this one. Simple, concise, easy to follow.

I Like.


Ryan Bates Jan 28, 2008 at 12:45

@Bryce, many developers don't like calling model finds directly from the view, but I usually don't have a problem with it. IMO views have as much access to models as controllers do in MVC.

But then you could say what's stopping the view from taking over the controllers role and doing all model processing? It's important to understand the role of a controller.

Don't think of the controller as a layer between the view and the model, instead think of it as a layer between the user and the application. The controller should process user input and filter that to the models and views. Many times this involves taking the params hash and fetching models for the view.

Notice in this case the @recent_products array had nothing to do with user input. Therefore it belongs in the view more than the controller. Why should the controller care that we want to display the recent products in addition to what the user requested?

I would have a much harder time moving the @product find itself into the view because this comes from the user input (params hash) and is directly what the user requested.

This is all my own theory. Other developers think differently so I encourage you to find what works best for you.

@chris, great question. Unfortunately I don't know the answer as I don't test caching. I imagine you will have to enable caching temporarily in your testing environment and check for the existance of the file. Alternatively you could use mocking and just make sure youre view calls the cache method with the proper parameters.


sthapit Jan 28, 2008 at 20:01

Great screencast - things worked without a flaw on my development machine and the cached partials are stored in /tmp/cache directory. I noticed, however, on the production machine that neither the cached partials nor the /tmp/cache directory exist - am I missing something or are my partials not getting cached? Thanks.

p.s. I have this line in environments/production.rb: config.action_controller.perform_caching = true so I believe caching is turned on.

/current/tmp: ls only shows
pids


Michel Jan 29, 2008 at 02:11

Ah the link is now working for me!

Thanks for the great screencasts!


Ryan Bates Jan 29, 2008 at 07:12

@sthapit, there are various places the cache could be stored. In development mode this defaults to a file store, but in production it defaults to a memory store. If you have a mongrel cluster then memory store probably isn't the best because it will need to be cached separately for each mongrel process and memory could get out of hand. Instead I recommend starting with a file based store by adding this line to your production.rb file:

config.action_controller.fragment_cache_store = [:file_store, "#{RAILS_ROOT}/tmp/cache"]


sthapit Jan 29, 2008 at 08:02

that makes perfect sense. yes, i'm using a mongrel cluster so i added that to production.rb and /tmp/cache now stores all the cached partials.

i'd also noticed that without that line the cached partial wouldn't expire with the sweeper consistently - but i think it makes sense now. it was probably not getting refreshed on all mongrels. but now that problem is gone as well. thanks!


Quint Jan 30, 2008 at 18:33

Where did you get the sweeper snippet for TextMate? Couldn't find it in the RoR bundle.


Ryan Bates Jan 31, 2008 at 17:26

@Quint, the sweeper snippet is custom. You can find it here.
http://pastie.caboo.se/144239


Lee Feb 23, 2008 at 09:19

Exactly what my app needed. Thanks


Designer Mar 18, 2008 at 03:39

Excellent stuff. Exactly what I needed. Thanks, Ryan!


Michael Voigt Mar 30, 2008 at 12:55

Hello,

nice screencast!!! I have a small question about the cache_sweeper. What i do, if i have more controllers, or conjobs to modify the product model. What is the best way to handle this fact?

Michael


Daniel Apr 04, 2008 at 08:18

Hello,

Very nice screencast!

Daniel


Julien Apr 12, 2008 at 18:09

Excellent screencast... however, I am facing a little problem here.

I've got a messages controller (and a message Model), an different methods among which index, unread, flagged... etc that performs different Message.find(...) but render messages in the same way, with a "respond_to", for each type of call (HTL, XML, or AJAX).

Should I put all the logic in the partial, within the cache block? It seems pretty awkward to do a case params[:action] in my view... and keeping index unread, flagged actions empty in my controller!

What would you do?


Julien Apr 12, 2008 at 18:44

Oh, and I forgot to say that this :

unless read_fragment(...)
@collection = Message.find(...)
end

approach deosn't do it for me... since, it messes up the respond_to.


kino May 04, 2008 at 03:27

In my project, there are many accounts. Each account has many users, but the fragments can be cached for all the users in an account, so putting the account id in the cache identifier is enough to make them unique.

So you could do

<% cache "recent_contacts_#{@account.id}" do %>
<%= render :partial => "/shared/contact", :collection => @contacts %>
<% end %>


Cristi May 17, 2008 at 13:09

Very useful screencast


creare site May 31, 2008 at 12:07

Thanks for information

Add your comment:

(SKIP THIS ONE)

(required)

(not shown)


(required)

subscribe:
sponsored by:
if you want to help:
required:
Get Quicktime Player