362 lines
13 KiB
Plaintext
362 lines
13 KiB
Plaintext
|
Caching with Rails: An overview
|
||
|
===============================
|
||
|
|
||
|
Everyone caches. This guide will teach you what you need to know about
|
||
|
avoiding that expensive round-trip to your database and returning what you
|
||
|
need to return to those hungry web clients in the shortest time possible.
|
||
|
|
||
|
== Basic Caching
|
||
|
|
||
|
This is an introduction to the three types of caching techniques that Rails
|
||
|
provides by default without the use of any third party plugins.
|
||
|
|
||
|
To get started make sure Base.perform_caching is set to true for your
|
||
|
environment.
|
||
|
|
||
|
[source, ruby]
|
||
|
-----------------------------------------------------
|
||
|
Base.perform_caching = true
|
||
|
-----------------------------------------------------
|
||
|
|
||
|
=== Page Caching
|
||
|
|
||
|
Page caching is a Rails mechanism which allows the request for a generated
|
||
|
page to be fulfilled by the webserver, without ever having to go through the
|
||
|
Rails stack at all. Obviously, this is super fast. Unfortunately, it can't be
|
||
|
applied to every situation (such as pages that need authentication) and since
|
||
|
the webserver is literally just serving a file from the filesystem, cache
|
||
|
expiration is an issue that needs to be dealt with.
|
||
|
|
||
|
So, how do you enable this super-fast cache behavior? Simple, let's say you
|
||
|
have a controller called ProductController and a 'list' action that lists all
|
||
|
the products
|
||
|
|
||
|
[source, ruby]
|
||
|
-----------------------------------------------------
|
||
|
class ProductController < ActionController
|
||
|
|
||
|
cache_page :list
|
||
|
|
||
|
def list; end
|
||
|
|
||
|
end
|
||
|
-----------------------------------------------------
|
||
|
|
||
|
The first time anyone requestsion products/list, Rails will generate a file
|
||
|
called list.html and the webserver will then look for that file before it
|
||
|
passes the next request for products/list to your Rails application.
|
||
|
|
||
|
By default, the page cache directory is set to Rails.public_path (which is
|
||
|
usually set to RAILS_ROOT + "/public") and this can be configured by changing
|
||
|
the configuration setting Base.cache_public_directory
|
||
|
|
||
|
The page caching mechanism will automatically add a .html exxtension to
|
||
|
requests for pages that do not have an extension to make it easy for the
|
||
|
webserver to find those pages and this can be configured by changing the
|
||
|
configuration setting Base.page_cache_extension
|
||
|
|
||
|
In order to expire this page when a new product is added we could extend our
|
||
|
example controler like this:
|
||
|
|
||
|
[source, ruby]
|
||
|
-----------------------------------------------------
|
||
|
class ProductController < ActionController
|
||
|
|
||
|
cache_page :list
|
||
|
|
||
|
def list; end
|
||
|
|
||
|
def create
|
||
|
expire_page :action => :list
|
||
|
end
|
||
|
|
||
|
end
|
||
|
-----------------------------------------------------
|
||
|
|
||
|
If you want a more complicated expiration scheme, you can use cache sweepers
|
||
|
to expire cached objects when things change. This is covered in the section on Sweepers.
|
||
|
|
||
|
[More: caching paginated results? more examples? Walk-through of page caching?]
|
||
|
|
||
|
=== Action Caching
|
||
|
|
||
|
One of the issues with page caching is that you cannot use it for pages that
|
||
|
require to restrict access somehow. This is where Action Caching comes in.
|
||
|
Action Caching works like Page Caching except for the fact that the incoming
|
||
|
web request does go from the webserver to the Rails stack and Action Pack so
|
||
|
that before_filters can be run on it before the cache is served, so that
|
||
|
authentication and other restrictions can be used while still serving the
|
||
|
result of the output from a cached copy.
|
||
|
|
||
|
Clearing the cache works in the exact same way as with Page Caching.
|
||
|
|
||
|
Let's say you only wanted authenticated users to edit or create a Product
|
||
|
object, but still cache those pages:
|
||
|
|
||
|
[source, ruby]
|
||
|
-----------------------------------------------------
|
||
|
class ProductController < ActionController
|
||
|
|
||
|
before_filter :authenticate, :only => [ :edit, :create ]
|
||
|
cache_page :list
|
||
|
caches_action :edit
|
||
|
|
||
|
def list; end
|
||
|
|
||
|
def create
|
||
|
expire_page :action => :list
|
||
|
expire_action :action => :edit
|
||
|
end
|
||
|
|
||
|
def edit; end
|
||
|
|
||
|
end
|
||
|
-----------------------------------------------------
|
||
|
|
||
|
And you can also use :if (or :unless) to pass a Proc that specifies when the
|
||
|
action should be cached. Also, you can use :layout => false to cache without
|
||
|
layout so that dynamic information in the layout such as logged in user info
|
||
|
or the number of items in the cart can be left uncached. This feature is
|
||
|
available as of Rails 2.2.
|
||
|
|
||
|
|
||
|
[More: more examples? Walk-through of action caching from request to response?
|
||
|
Description of Rake tasks to clear cached files? Show example of
|
||
|
subdomain caching? Talk about :cache_path, :if and assing blocks/Procs
|
||
|
to expire_action?]
|
||
|
|
||
|
=== Fragment Caching
|
||
|
|
||
|
Life would be perfect if we could get away with caching the entire contents of
|
||
|
a page or action and serving it out to the world. Unfortunately, dynamic web
|
||
|
applications usually build pages with a variety of components not all of which
|
||
|
have the same caching characteristics. In order to address such a dynamically
|
||
|
created page where different parts of the page need to be cached and expired
|
||
|
differently Rails provides a mechanism called Fragment caching.
|
||
|
|
||
|
Fragment caching allows a fragment of view logic to be wrapped in a cache
|
||
|
block and served out of the cache store when the next request comes in.
|
||
|
|
||
|
As an example, if you wanted to show all the orders placed on your website in
|
||
|
real time and didn't want to cache that part of the page, but did want to
|
||
|
cache the part of the page which lists all products available, you could use
|
||
|
this piece of code:
|
||
|
|
||
|
[source, ruby]
|
||
|
-----------------------------------------------------
|
||
|
<% Order.find_recent.each do |o| %>
|
||
|
<%= o.buyer.name %> bought <% o.product.name %>
|
||
|
<% end %>
|
||
|
|
||
|
<% cache do %>
|
||
|
All available products:
|
||
|
<% Product.find(:all).each do |p| %>
|
||
|
<%= link_to p.name, product_url(p) %>
|
||
|
<% end %>
|
||
|
<% end %>
|
||
|
-----------------------------------------------------
|
||
|
|
||
|
The cache block in our example will bind to the action that called it and is
|
||
|
written out to the same place as the Action Cache, which means that if you
|
||
|
want to cache multiple fragments per action, you should provide an action_path to the cache call:
|
||
|
|
||
|
[source, ruby]
|
||
|
-----------------------------------------------------
|
||
|
<% cache(:action => 'recent', :action_suffix => 'all_products') do %>
|
||
|
All available products:
|
||
|
-----------------------------------------------------
|
||
|
|
||
|
and you can expire it using the expire_fragment method, like so:
|
||
|
|
||
|
[source, ruby]
|
||
|
-----------------------------------------------------
|
||
|
expire_fragment(:controller => 'producst', :action => 'recent', :action_suffix => 'all_products)
|
||
|
-----------------------------------------------------
|
||
|
|
||
|
[More: more examples? description of fragment keys and expiration, etc? pagination?]
|
||
|
|
||
|
=== Sweepers
|
||
|
|
||
|
Cache sweeping is a mechanism which allows you to get around having a ton of
|
||
|
expire_{page,action,fragment} calls in your code by moving all the work
|
||
|
required to expire cached content into a ActionController::Caching::Sweeper
|
||
|
class that is an Observer and looks for changes to an object via callbacks,
|
||
|
and when a change occurs it expires the caches associated with that object n
|
||
|
an around or after filter.
|
||
|
|
||
|
Continuing with our Product controller example, we could rewrite it with a
|
||
|
sweeper such as the following:
|
||
|
|
||
|
[source, ruby]
|
||
|
-----------------------------------------------------
|
||
|
class StoreSweeper < ActionController::Caching::Sweeper
|
||
|
observe Product # This sweeper is going to keep an eye on the Post model
|
||
|
|
||
|
# If our sweeper detects that a Post was created call this
|
||
|
def after_create(product)
|
||
|
expire_cache_for(product)
|
||
|
end
|
||
|
|
||
|
# If our sweeper detects that a Post was updated call this
|
||
|
def after_update(product)
|
||
|
expire_cache_for(product)
|
||
|
end
|
||
|
|
||
|
# If our sweeper detects that a Post was deleted call this
|
||
|
def after_destroy(product)
|
||
|
expire_cache_for(product)
|
||
|
end
|
||
|
|
||
|
private
|
||
|
def expire_cache_for(record)
|
||
|
# Expire the list page now that we added a new product
|
||
|
expire_page(:controller => '#{record}', :action => 'list')
|
||
|
|
||
|
# Expire a fragment
|
||
|
expire_fragment(:controller => '#{record}', :action => 'recent', :action_suffix => 'all_products')
|
||
|
end
|
||
|
end
|
||
|
-----------------------------------------------------
|
||
|
|
||
|
Then we add it to our controller to tell it to call the sweeper when certain
|
||
|
actions are called. So, if we wanted to expire the cached content for the
|
||
|
list and edit actions when the create action was called, we could do the
|
||
|
following:
|
||
|
|
||
|
[source, ruby]
|
||
|
-----------------------------------------------------
|
||
|
class ProductController < ActionController
|
||
|
|
||
|
before_filter :authenticate, :only => [ :edit, :create ]
|
||
|
cache_page :list
|
||
|
caches_action :edit
|
||
|
cache_sweeper :store_sweeper, :only => [ :create ]
|
||
|
|
||
|
def list; end
|
||
|
|
||
|
def create
|
||
|
expire_page :action => :list
|
||
|
expire_action :action => :edit
|
||
|
end
|
||
|
|
||
|
def edit; end
|
||
|
|
||
|
end
|
||
|
-----------------------------------------------------
|
||
|
|
||
|
[More: more examples? better sweepers?]
|
||
|
|
||
|
=== SQL Caching
|
||
|
|
||
|
Query caching is a Rails feature that caches the result set returned by each
|
||
|
query so that if Rails encounters the same query again for that request, it
|
||
|
will used the cached result set as opposed to running the query against the
|
||
|
database again.
|
||
|
|
||
|
For example:
|
||
|
|
||
|
[source, ruby]
|
||
|
-----------------------------------------------------
|
||
|
class ProductController < ActionController
|
||
|
|
||
|
before_filter :authenticate, :only => [ :edit, :create ]
|
||
|
cache_page :list
|
||
|
caches_action :edit
|
||
|
cache_sweeper :store_sweeper, :only => [ :create ]
|
||
|
|
||
|
def list
|
||
|
# Run a find query
|
||
|
Product.find(:all)
|
||
|
|
||
|
...
|
||
|
|
||
|
# Run the same query again
|
||
|
Product.find(:all)
|
||
|
end
|
||
|
|
||
|
def create
|
||
|
expire_page :action => :list
|
||
|
expire_action :action => :edit
|
||
|
end
|
||
|
|
||
|
def edit; end
|
||
|
|
||
|
end
|
||
|
-----------------------------------------------------
|
||
|
|
||
|
In the 'list' action above, the result set returned by the first
|
||
|
Product.find(:all) will be cached and will be used to avoid querying the
|
||
|
database again the second time that finder is called.
|
||
|
|
||
|
Query caches are created at the start of an action and destroyed at the end of
|
||
|
that action and thus persist only for the duration of the action.
|
||
|
|
||
|
=== Cache stores
|
||
|
|
||
|
Rails provides different stores for the cached data for action and fragment
|
||
|
caches. Page caches are always stored on disk.
|
||
|
|
||
|
The cache stores provided include:
|
||
|
|
||
|
1) Memory store: Cached data is stored in the memory allocated to the Rails
|
||
|
process, which is fine for WEBrick and for FCGI (if you
|
||
|
don't care that each FCGI process holds its own fragment
|
||
|
store). It's not suitable for CGI as the process is thrown
|
||
|
away at the end of each request. It can potentially also
|
||
|
take up a lot of memory since each process keeps all the
|
||
|
caches in memory.
|
||
|
|
||
|
[source, ruby]
|
||
|
-----------------------------------------------------
|
||
|
ActionController::Base.cache_store = :memory_store
|
||
|
-----------------------------------------------------
|
||
|
|
||
|
2) File store: Cached data is stored on the disk, this is the default store
|
||
|
and the default path for this store is: /tmp/cache. Works
|
||
|
well for all types of environments and allows all processes
|
||
|
running from the same application directory to access the
|
||
|
cached content.
|
||
|
|
||
|
|
||
|
[source, ruby]
|
||
|
-----------------------------------------------------
|
||
|
ActionController::Base.cache_store = :file_store, "/path/to/cache/directory"
|
||
|
-----------------------------------------------------
|
||
|
|
||
|
3) DRb store: Cached data is stored in a separate shared DRb process that all
|
||
|
servers communicate with. This works for all environments and
|
||
|
only keeps one cache around for all processes, but requires
|
||
|
that you run and manage a separate DRb process.
|
||
|
|
||
|
[source, ruby]
|
||
|
-----------------------------------------------------
|
||
|
ActionController::Base.cache_store = :drb_store, "druby://localhost:9192"
|
||
|
-----------------------------------------------------
|
||
|
|
||
|
4) MemCached store: Works like DRbStore, but uses Danga's MemCache instead.
|
||
|
Requires the ruby-memcache library:
|
||
|
gem install ruby-memcache.
|
||
|
|
||
|
[source, ruby]
|
||
|
-----------------------------------------------------
|
||
|
ActionController::Base.cache_store = :mem_cache_store, "localhost"
|
||
|
-----------------------------------------------------
|
||
|
|
||
|
5) Custom store: You can define your own cache store (new in Rails 2.1)
|
||
|
|
||
|
[source, ruby]
|
||
|
-----------------------------------------------------
|
||
|
ActionController::Base.cache_store = MyOwnStore.new("parameter")
|
||
|
-----------------------------------------------------
|
||
|
|
||
|
== Advanced Caching
|
||
|
|
||
|
Along with the built-in mechanisms outlined above, a number of excellent
|
||
|
plugins exist to help with finer grained control over caching. These include
|
||
|
Chris Wanstrath's excellent cache_fu plugin (more info here:
|
||
|
http://errtheblog.com/posts/57-kickin-ass-w-cachefu) and Evan Weaver's
|
||
|
interlock plugin (more info here:
|
||
|
http://blog.evanweaver.com/articles/2007/12/13/better-rails-caching/). Both
|
||
|
of these plugins play nice with memcached and are a must-see for anyone
|
||
|
seriously considering optimizing their caching needs.
|