diff options
Diffstat (limited to 'actionview/lib/action_view/helpers/cache_helper.rb')
-rw-r--r-- | actionview/lib/action_view/helpers/cache_helper.rb | 151 |
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? |