aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_view/helpers/cache_helper.rb
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_view/helpers/cache_helper.rb')
-rw-r--r--actionpack/lib/action_view/helpers/cache_helper.rb168
1 files changed, 150 insertions, 18 deletions
diff --git a/actionpack/lib/action_view/helpers/cache_helper.rb b/actionpack/lib/action_view/helpers/cache_helper.rb
index 850dd5f448..995aa10afb 100644
--- a/actionpack/lib/action_view/helpers/cache_helper.rb
+++ b/actionpack/lib/action_view/helpers/cache_helper.rb
@@ -2,38 +2,117 @@ module ActionView
# = Action View Cache Helper
module Helpers
module CacheHelper
- # This helper exposes a method for caching fragments of a view
+ # This helper exposes a method for caching fragments of a view
# rather than an entire action or page. This technique is useful
# caching pieces like menus, lists of newstopics, static HTML
# fragments, and so on. This method takes a block that contains
- # the content you wish to cache.
+ # the content you wish to cache.
#
- # See ActionController::Caching::Fragments for usage instructions.
+ # The best way to use this is by doing key-based cache expiration
+ # on top of a cache store like Memcached that'll automatically
+ # kick out old entries. For more on key-based expiration, see:
+ # http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works
#
- # ==== Examples
- # If you want to cache a navigation menu, you can do following:
+ # When using this method, you list the cache dependency as the name of the cache, like so:
#
- # <% cache do %>
- # <%= render :partial => "menu" %>
+ # <% cache project do %>
+ # <b>All the topics on this project</b>
+ # <%= render project.topics %>
# <% end %>
#
- # You can also cache static content:
+ # This approach will assume that when a new topic is added, you'll touch
+ # the project. The cache key generated from this call will be something like:
#
- # <% cache do %>
- # <p>Hello users! Welcome to our website!</p>
+ # views/projects/123-20120806214154/7a1156131a6928cb0026877f8b749ac9
+ # ^class ^id ^updated_at ^template tree digest
+ #
+ # The cache is thus automatically bumped whenever the project updated_at is touched.
+ #
+ # If your template cache depends on multiple sources (try to avoid this to keep things simple),
+ # you can name all these dependencies as part of an array:
+ #
+ # <% cache [ project, current_user ] do %>
+ # <b>All the topics on this project</b>
+ # <%= render project.topics %>
# <% end %>
#
- # Static content with embedded ruby content can be cached as
- # well:
+ # This will include both records as part of the cache key and updating either of them will
+ # expire the cache.
+ #
+ # ==== Template digest
+ #
+ # The template digest that's added to the cache key is computed by taking an md5 of the
+ # contents of the entire template file. This ensures that your caches will automatically
+ # expire when you change the template file.
+ #
+ # Note that the md5 is taken of the entire template file, not just what's within the
+ # cache do/end call. So it's possible that changing something outside of that call will
+ # still expire the cache.
+ #
+ # Additionally, the digestor will automatically look through your template file for
+ # explicit and implicit dependencies, and include those as part of the digest.
+ #
+ # The digestor can be bypassed by passing skip_digest: true as an option to the cache call:
+ #
+ # <% cache project, skip_digest: true do %>
+ # <b>All the topics on this project</b>
+ # <%= render project.topics %>
+ # <% end %>
+ #
+ # ==== Implicit dependencies
+ #
+ # Most template dependencies can be derived from calls to render in the template itself.
+ # Here are some examples of render calls that Cache Digests knows how to decode:
+ #
+ # render partial: "comments/comment", collection: commentable.comments
+ # render "comments/comments"
+ # render 'comments/comments'
+ # render('comments/comments')
+ #
+ # render "header" => render("comments/header")
+ #
+ # render(@topic) => render("topics/topic")
+ # render(topics) => render("topics/topic")
+ # render(message.topics) => render("topics/topic")
+ #
+ # It's not possible to derive all render calls like that, though. Here are a few examples of things that can't be derived:
+ #
+ # render group_of_attachments
+ # render @project.documents.where(published: true).order('created_at')
+ #
+ # You will have to rewrite those to the explicit form:
+ #
+ # render partial: 'attachments/attachment', collection: group_of_attachments
+ # render partial: 'documents/document', collection: @project.documents.where(published: true).order('created_at')
+ #
+ # === Explicit dependencies
+ #
+ # Some times you'll have template dependencies that can't be derived at all. This is typically
+ # the case when you have template rendering that happens in helpers. Here's an example:
+ #
+ # <%= render_sortable_todolists @project.todolists %>
+ #
+ # You'll need to use a special comment format to call those out:
+ #
+ # <%# Template Dependency: todolists/todolist %>
+ # <%= render_sortable_todolists @project.todolists %>
+ #
+ # The pattern used to match these is /# Template Dependency: ([^ ]+)/, so it's important that you type it out just so.
+ # You can only declare one template dependency per line.
+ #
+ # === External dependencies
#
- # <% cache do %>
- # Topics:
- # <%= render :partial => "topics", :collection => @topic_list %>
- # <i>Topics listed alphabetically</i>
- # <% end %>
+ # If you use a helper method, for example, inside of a cached block and you then update that helper,
+ # you'll have to bump the cache as well. It doesn't really matter how you do it, but the md5 of the template file
+ # must change. One recommendation is to simply be explicit in a comment, like:
+ #
+ # <%# Helper Dependency Updated: May 6, 2012 at 6pm %>
+ # <%= some_helper_method(person) %>
+ #
+ # Now all you'll have to do is change that timestamp when the helper method changes.
def cache(name = {}, options = nil, &block)
if controller.perform_caching
- safe_concat(fragment_for(name, options, &block))
+ safe_concat(fragment_for(cache_fragment_name(name, options), options, &block))
else
yield
end
@@ -41,7 +120,60 @@ module ActionView
nil
end
+ # Cache fragments of a view if +condition+ is true
+ #
+ # <%= cache_if admin?, project do %>
+ # <b>All the topics on this project</b>
+ # <%= render project.topics %>
+ # <% end %>
+ def cache_if(condition, name = {}, options = nil, &block)
+ if condition
+ cache(name, options, &block)
+ else
+ yield
+ end
+
+ nil
+ end
+
+ # Cache fragments of a view unless +condition+ is true
+ #
+ # <%= cache_unless admin?, project do %>
+ # <b>All the topics on this project</b>
+ # <%= render project.topics %>
+ # <% end %>
+ def cache_unless(condition, name = {}, options = nil, &block)
+ cache_if !condition, name, options, &block
+ end
+
+ # This helper returns the name of a cache key for a given fragment cache
+ # call. By supplying skip_digest: true to cache, the digestion of cache
+ # fragments can be manually bypassed. This is useful when cache fragments
+ # cannot be manually expired unless you know the exact key which is the
+ # case when using memcached.
+ def cache_fragment_name(name = {}, options = nil)
+ skip_digest = options && options[:skip_digest]
+
+ if skip_digest
+ name
+ else
+ fragment_name_with_digest(name)
+ end
+ end
+
private
+
+ def fragment_name_with_digest(name) #:nodoc:
+ if @virtual_path
+ [
+ *Array(name.is_a?(Hash) ? controller.url_for(name).split("://").last : name),
+ Digestor.digest(@virtual_path, formats.last.to_sym, lookup_context)
+ ]
+ else
+ name
+ end
+ end
+
# TODO: Create an object that has caching read/write on it
def fragment_for(name = {}, options = nil, &block) #:nodoc:
if fragment = controller.read_fragment(name, options)