aboutsummaryrefslogtreecommitdiffstats
path: root/actionview/lib/action_view/helpers/cache_helper.rb
diff options
context:
space:
mode:
Diffstat (limited to 'actionview/lib/action_view/helpers/cache_helper.rb')
-rw-r--r--actionview/lib/action_view/helpers/cache_helper.rb151
1 files changed, 85 insertions, 66 deletions
diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb
index 797d029317..b1a14250c3 100644
--- a/actionview/lib/action_view/helpers/cache_helper.rb
+++ b/actionview/lib/action_view/helpers/cache_helper.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
module ActionView
# = Action View Cache Helper
- module Helpers
+ module Helpers #:nodoc:
module CacheHelper
# This helper exposes a method for caching fragments of a view
# rather than an entire action or page. This technique is useful
@@ -8,10 +10,9 @@ module ActionView
# fragments, and so on. This method takes a block that contains
# the content you wish to cache.
#
- # 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://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works
+ # The best way to use this is by doing recyclable key-based cache expiration
+ # on top of a cache store like Memcached or Redis that'll automatically
+ # kick out old entries.
#
# When using this method, you list the cache dependency as the name of the cache, like so:
#
@@ -23,10 +24,14 @@ module ActionView
# 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:
#
- # views/projects/123-20120806214154/7a1156131a6928cb0026877f8b749ac9
- # ^class ^id ^updated_at ^template tree digest
+ # views/template/action.html.erb:7a1156131a6928cb0026877f8b749ac9/projects/123
+ # ^template path ^template tree digest ^class ^id
#
- # The cache is thus automatically bumped whenever the project updated_at is touched.
+ # This cache key is stable, but it's combined with a cache version derived from the project
+ # record. When the project updated_at is touched, the #cache_version changes, even
+ # if the key stays stable. This means that unlike a traditional key-based cache expiration
+ # approach, you won't be generating cache trash, unused keys, simply because the dependent
+ # record is updated.
#
# 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:
@@ -41,11 +46,11 @@ module ActionView
#
# ==== \Template digest
#
- # The template digest that's added to the cache key is computed by taking an md5 of the
+ # 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
+ # 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.
#
@@ -69,11 +74,11 @@ module ActionView
# render 'comments/comments'
# render('comments/comments')
#
- # render "header" => render("comments/header")
+ # render "header" translates to render("comments/header")
#
- # render(@topic) => render("topics/topic")
- # render(topics) => render("topics/topic")
- # render(message.topics) => render("topics/topic")
+ # render(@topic) translates to render("topics/topic")
+ # render(topics) translates to render("topics/topic")
+ # render(message.topics) translates to 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:
@@ -88,7 +93,7 @@ module ActionView
#
# === Explicit dependencies
#
- # Some times you'll have template dependencies that can't be derived at all. This is typically
+ # Sometimes 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 %>
@@ -98,7 +103,19 @@ module ActionView
# <%# Template Dependency: todolists/todolist %>
# <%= render_sortable_todolists @project.todolists %>
#
- # The pattern used to match these is <tt>/# Template Dependency: (\S+)/</tt>,
+ # In some cases, like a single table inheritance setup, you might have
+ # a bunch of explicit dependencies. Instead of writing every template out,
+ # you can use a wildcard to match any template in a directory:
+ #
+ # <%# Template Dependency: events/* %>
+ # <%= render_categorizable_events @person.events %>
+ #
+ # This marks every template in the directory as a dependency. To find those
+ # templates, the wildcard path must be absolutely defined from <tt>app/views</tt> or paths
+ # otherwise added with +prepend_view_path+ or +append_view_path+.
+ # This way the wildcard for <tt>app/views/recordings/events</tt> would be <tt>recordings/events/*</tt> etc.
+ #
+ # The pattern used to match explicit dependencies is <tt>/# Template Dependency: (\S+)/</tt>,
# so it's important that you type it out just so.
# You can only declare one template dependency per line.
#
@@ -106,7 +123,7 @@ module ActionView
#
# If you use a helper method, for example, inside 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
+ # 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 %>
@@ -114,47 +131,42 @@ module ActionView
#
# Now all you have to do is change that timestamp when the helper method changes.
#
- # === Automatic Collection Caching
- #
- # When rendering collections such as:
+ # === Collection Caching
#
- # <%= render @notifications %>
- # <%= render partial: 'notifications/notification', collection: @notifications %>
+ # When rendering a collection of objects that each use the same partial, a <tt>:cached</tt>
+ # option can be passed.
#
- # If the notifications/_notification partial starts with a cache call as:
+ # For collections rendered such:
#
- # <% cache notification do %>
- # <%= notification.name %>
- # <% end %>
+ # <%= render partial: 'projects/project', collection: @projects, cached: true %>
#
- # The collection can then automatically use any cached renders for that
- # template by reading them at once instead of one by one.
+ # The <tt>cached: true</tt> will make Action View's rendering read several templates
+ # from cache at once instead of one call per template.
#
- # See ActionView::Template::Handlers::ERB.resource_cache_call_pattern for
- # more information on what cache calls make a template eligible for this
- # collection caching.
+ # Templates in the collection not already cached are written to cache.
#
- # The automatic cache multi read can be turned off like so:
+ # Works great alongside individual template fragment caching.
+ # For instance if the template the collection renders is cached like:
#
- # <%= render @notifications, cache: false %>
+ # # projects/_project.html.erb
+ # <% cache project do %>
+ # <%# ... %>
+ # <% end %>
#
- # === Explicit Collection Caching
+ # Any collection renders will find those cached templates when attempting
+ # to read multiple templates at once.
#
- # If the partial template doesn't start with a clean cache call as
- # mentioned above, you can still benefit from collection caching by
- # adding a special comment format anywhere in the template, like:
+ # If your collection cache depends on multiple sources (try to avoid this to keep things simple),
+ # you can name all these dependencies as part of a block that returns an array:
#
- # <%# Template Collection: notification %>
- # <% my_helper_that_calls_cache(some_arg, notification) do %>
- # <%= notification.name %>
- # <% end %>
+ # <%= render partial: 'projects/project', collection: @projects, cached: -> project { [ project, current_user ] } %>
#
- # The pattern used to match these is <tt>/# Template Collection: (\S+)/</tt>,
- # so it's important that you type it out just so.
- # You can only declare one collection in a partial template file.
+ # This will include both records as part of the cache key and updating either of them will
+ # expire the cache.
def cache(name = {}, options = {}, &block)
if controller.respond_to?(:perform_caching) && controller.perform_caching
- safe_concat(fragment_for(cache_fragment_name(name, options), options, &block))
+ name_options = options.slice(:skip_digest, :virtual_path)
+ safe_concat(fragment_for(cache_fragment_name(name, name_options), options, &block))
else
yield
end
@@ -189,55 +201,62 @@ module ActionView
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
+ # call. By supplying <tt>skip_digest: true</tt> 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.
#
# The digest will be generated using +virtual_path:+ if it is provided.
#
- def cache_fragment_name(name = {}, skip_digest: nil, virtual_path: nil)
+ def cache_fragment_name(name = {}, skip_digest: nil, virtual_path: nil, digest_path: nil)
if skip_digest
name
else
- fragment_name_with_digest(name, virtual_path)
+ fragment_name_with_digest(name, virtual_path, digest_path)
end
end
- # Given a key (as described in ActionController::Caching::Fragments.expire_fragment),
- # returns a key suitable for use in reading, writing, or expiring a
- # cached fragment. All keys are prefixed with <tt>views/</tt> and uses
- # ActiveSupport::Cache.expand_cache_key for the expansion.
- def fragment_cache_key(key)
- ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views)
+ def digest_path_from_virtual(virtual_path) # :nodoc:
+ digest = Digestor.digest(name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies)
+
+ if digest.present?
+ "#{virtual_path}:#{digest}"
+ else
+ virtual_path
+ end
end
private
- def fragment_name_with_digest(name, virtual_path) #:nodoc:
+ def fragment_name_with_digest(name, virtual_path, digest_path)
virtual_path ||= @virtual_path
- if virtual_path
- names = Array(name.is_a?(Hash) ? controller.url_for(name).split("://").last : name)
- digest = Digestor.digest name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies
- [ *names, digest ]
+ if virtual_path || digest_path
+ name = controller.url_for(name).split("://").last if name.is_a?(Hash)
+
+ digest_path ||= digest_path_from_virtual(virtual_path)
+
+ [ digest_path, name ]
else
name
end
end
- # TODO: Create an object that has caching read/write on it
- def fragment_for(name = {}, options = nil, &block) #:nodoc:
- read_fragment_for(name, options) || write_fragment_for(name, options, &block)
+ def fragment_for(name = {}, options = nil, &block)
+ if content = read_fragment_for(name, options)
+ @view_renderer.cache_hits[@virtual_path] = :hit if defined?(@view_renderer)
+ content
+ else
+ @view_renderer.cache_hits[@virtual_path] = :miss if defined?(@view_renderer)
+ write_fragment_for(name, options, &block)
+ end
end
- def read_fragment_for(name, options) #:nodoc:
+ def read_fragment_for(name, options)
controller.read_fragment(name, options)
end
- def write_fragment_for(name, options) #:nodoc:
- # VIEW TODO: Make #capture usable outside of ERB
- # This dance is needed because Builder can't use capture
+ def write_fragment_for(name, options)
pos = output_buffer.length
yield
output_safe = output_buffer.html_safe?