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:
parent
e56c635427
commit
11644fd0ce
11 changed files with 167 additions and 3 deletions
|
@ -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
|
||||
|
|
1
actionpack/test/fixtures/collection_cache/index.html.erb
vendored
Normal file
1
actionpack/test/fixtures/collection_cache/index.html.erb
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<%= render @customers %>
|
4
actionpack/test/fixtures/customers/_commented_customer.html.erb
vendored
Normal file
4
actionpack/test/fixtures/customers/_commented_customer.html.erb
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
<%# I'm a comment %>
|
||||
<% cache customer do %>
|
||||
<%= customer.name %>, <%= customer.id %>
|
||||
<% end %>
|
3
actionpack/test/fixtures/customers/_customer.html.erb
vendored
Normal file
3
actionpack/test/fixtures/customers/_customer.html.erb
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
<% cache customer do %>
|
||||
<%= customer.name %>, <%= customer.id %>
|
||||
<% end %>
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
3
actionview/test/fixtures/test/_cached_customer.erb
vendored
Normal file
3
actionview/test/fixtures/test/_cached_customer.erb
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
<% cache cached_customer do %>
|
||||
Hello: <%= cached_customer.name %>
|
||||
<% end %>
|
3
actionview/test/fixtures/test/_cached_customer_as.erb
vendored
Normal file
3
actionview/test/fixtures/test/_cached_customer_as.erb
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
<% cache buyer do %>
|
||||
<%= greeting %>: <%= customer.name %>
|
||||
<% end %>
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue