1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Collections automatically cache and fetch partials.

Collections can take advantage of `multi_read` if they render one template
and their partials begin with a cache call.

The cache call must correspond to either what the collections elements are
rendered as, or match the inferred name of the partial.

So with a notifications/_notification.html.erb template like:

```ruby
<% cache notification %>
  <%# ... %>
<% end %>
```

A collection would be able to use `multi_read` if rendered like:

```ruby
<%= render @notifications %>
<%= render partial: 'notifications/notification', collection: @notifications, as: :notification %>
```
This commit is contained in:
Kasper Timm Hansen 2015-02-15 22:39:04 +01:00
parent e56c635427
commit 11644fd0ce
11 changed files with 167 additions and 3 deletions

View file

@ -1,5 +1,6 @@
require 'fileutils'
require 'abstract_unit'
require 'lib/controller/fake_models'
CACHE_DIR = 'test_cache'
# Don't change '/../temp/' cavalierly or you might hose something you don't want hosed
@ -349,3 +350,60 @@ class ViewCacheDependencyTest < ActionController::TestCase
assert_equal %w(trombone flute), HasDependenciesController.new.view_cache_dependencies
end
end
class CollectionCacheController < ActionController::Base
def index
@customers = [Customer.new('david', params[:id] || 1)]
end
def index_ordered
@customers = [Customer.new('david', 1), Customer.new('david', 2), Customer.new('david', 3)]
render 'index'
end
def index_explicit_render
@customers = [Customer.new('david', 1)]
render partial: 'customers/customer', collection: @customers
end
def index_with_comment
@customers = [Customer.new('david', 1)]
render partial: 'customers/commented_customer', collection: @customers, as: :customer
end
end
class AutomaticCollectionCacheTest < ActionController::TestCase
def setup
super
@controller = CollectionCacheController.new
@controller.perform_caching = true
@controller.cache_store = ActiveSupport::Cache::MemoryStore.new
end
def test_collection_fetches_cached_views
get :index
ActionView::PartialRenderer.expects(:collection_with_template).never
get :index
end
def test_preserves_order_when_reading_from_cache_plus_rendering
get :index, params: { id: 2 }
get :index_ordered
assert_select ':root', "david, 1\n david, 2\n david, 3"
end
def test_explicit_render_call_with_options
get :index_explicit_render
assert_select ':root', "david, 1"
end
def test_caching_works_with_beginning_comment
get :index_with_comment
ActionView::PartialRenderer.expects(:collection_with_template).never
get :index_with_comment
end
end

View file

@ -0,0 +1 @@
<%= render @customers %>

View file

@ -0,0 +1,4 @@
<%# I'm a comment %>
<% cache customer do %>
<%= customer.name %>, <%= customer.id %>
<% end %>

View file

@ -0,0 +1,3 @@
<% cache customer do %>
<%= customer.name %>, <%= customer.id %>
<% end %>

View file

@ -110,6 +110,29 @@ module ActionView
# <%= some_helper_method(person) %>
#
# Now all you'll have to do is change that timestamp when the helper method changes.
#
# === Automatic Collection Caching
#
# When rendering collections such as:
#
# <%= render @notifications %>
# <%= render partial: 'notifications/notification', collection: @notifications %>
#
# If the notifications/_notification partial starts with a cache call like so:
#
# <% cache notification do %>
# <%= notification.name %>
# <% end %>
#
# The collection can then automatically use any cached renders for that
# template by reading them at once instead of one by one.
#
# See ActionView::Template::Handlers::ERB.resource_cache_call_pattern for more
# information on what cache calls make a template eligible for this collection caching.
#
# The automatic cache multi read can be turned off like so:
#
# <%= render @notifications, cache: false %>
def cache(name = {}, options = nil, &block)
if controller.perform_caching
safe_concat(fragment_for(cache_fragment_name(name, options), options, &block))

View file

@ -26,11 +26,24 @@ module ActionView
end
def cache_collection?
@options[:cache].present?
@options.fetch(:cache, automatic_cache_eligible?)
end
def automatic_cache_eligible?
single_template_render? && !callable_cache_key? &&
@template.eligible_for_collection_caching?(as: @options[:as])
end
def single_template_render?
@template # Template is only set when a collection renders one template.
end
def callable_cache_key?
@options[:cache].respond_to?(:call)
end
def collection_by_cache_keys
seed = @options[:cache].respond_to?(:call) ? @options[:cache] : ->(i) { i }
seed = callable_cache_key? ? @options[:cache] : ->(i) { i }
@collection.each_with_object({}) do |item, hash|
hash[expanded_cache_key(seed.call(item))] = item

View file

@ -117,6 +117,7 @@ module ActionView
@source = source
@identifier = identifier
@handler = handler
@cache_name = extract_resource_cache_call_name
@compiled = false
@original_encoding = nil
@locals = details[:locals] || []
@ -152,6 +153,10 @@ module ActionView
@type ||= Types[@formats.first] if @formats.first
end
def eligible_for_collection_caching?(as: nil)
@cache_name == (as || inferred_cache_name).to_s
end
# Receives a view object and return a template similar to self by using @virtual_path.
#
# This method is useful if you have a template object but it does not contain its source
@ -332,5 +337,14 @@ module ActionView
payload = { virtual_path: @virtual_path, identifier: @identifier }
ActiveSupport::Notifications.instrument("#{action}.action_view", payload, &block)
end
def extract_resource_cache_call_name
$1 if @handler.respond_to?(:resource_cache_call_pattern) &&
@source =~ @handler.resource_cache_call_pattern
end
def inferred_cache_name
@inferred_cache_name ||= @virtual_path.split('/').last.sub('_', '')
end
end
end

View file

@ -123,6 +123,24 @@ module ActionView
).src
end
# Returns Regexp to extract a cached resource's name from a cache call at the
# first line of a template.
# The extracted cache name is expected in $1.
#
# <% cache notification do %> # => notification
#
# The pattern should support templates with a beginning comment:
#
# <%# Still extractable even though there's a comment %>
# <% cache notification do %> # => notification
#
# But fail to extract a name if a resource association is cached.
#
# <% cache notification.event do %> # => nil
def resource_cache_call_pattern
/\A(?:<%#.*%>\n?)?<% cache\(?\s*(\w+\.?)/
end
private
def valid_encoding(string, encoding)

View file

@ -0,0 +1,3 @@
<% cache cached_customer do %>
Hello: <%= cached_customer.name %>
<% end %>

View file

@ -0,0 +1,3 @@
<% cache buyer do %>
<%= greeting %>: <%= customer.name %>
<% end %>

View file

@ -599,6 +599,12 @@ class LazyViewRenderTest < ActiveSupport::TestCase
end
class CachedCollectionViewRenderTest < CachedViewRenderTest
class CachedCustomer < Customer; end
teardown do
ActionView::PartialRenderer.collection_cache.clear
end
test "with custom key" do
customer = Customer.new("david")
key = ActionController::Base.new.fragment_cache_key([customer, 'key'])
@ -607,7 +613,25 @@ class CachedCollectionViewRenderTest < CachedViewRenderTest
assert_equal "Hello",
@view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'key'] })
end
ActionView::PartialRenderer.collection_cache.clear
test "automatic caching with inferred cache name" do
customer = CachedCustomer.new("david")
key = ActionController::Base.new.fragment_cache_key(customer)
ActionView::PartialRenderer.collection_cache.write(key, 'Cached')
assert_equal "Cached",
@view.render(partial: "test/cached_customer", collection: [customer])
end
test "automatic caching with as name" do
customer = CachedCustomer.new("david")
key = ActionController::Base.new.fragment_cache_key(customer)
ActionView::PartialRenderer.collection_cache.write(key, 'Cached')
assert_equal "Cached",
@view.render(partial: "test/cached_customer_as", collection: [customer], as: :buyer)
end
end