Skip to content

Commit 0ef8165

Browse files
committed
Fix up caching, especially fragment_cache
1 parent d191342 commit 0ef8165

File tree

5 files changed

+68
-77
lines changed

5 files changed

+68
-77
lines changed

lib/active_model/serializer.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def serializable_hash(adapter_options = nil, options = {}, adapter_instance = se
167167
adapter_options ||= {}
168168
options[:include_directive] ||= ActiveModel::Serializer.include_directive_from_options(adapter_options)
169169
cached_attributes = adapter_options[:cached_attributes] ||= {}
170-
resource = cached_attributes(options[:fields], cached_attributes, adapter_instance)
170+
resource = fetch_attributes(options[:fields], cached_attributes, adapter_instance)
171171
relationships = resource_relationships(adapter_options, options, adapter_instance)
172172
resource.merge(relationships)
173173
end
@@ -195,6 +195,8 @@ def read_attribute_for_serialization(attr)
195195
if respond_to?(attr)
196196
send(attr)
197197
elsif self.class._fragmented
198+
# Attribute method wasn't available on this (fragment cached) serializer,
199+
# so read it from the original serializer it was based on.
198200
self.class._fragmented.read_attribute_for_serialization(attr)
199201
else
200202
object.read_attribute_for_serialization(attr)

lib/active_model/serializer/caching.rb

Lines changed: 41 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ module Caching
99
included do
1010
with_options instance_writer: false, instance_reader: false do |serializer|
1111
serializer.class_attribute :_cache # @api private : the cache store
12-
serializer.class_attribute :_fragmented # @api private : @see ::fragmented
12+
serializer.class_attribute :_fragmented # @api private : Used ONLY by FragmentedSerializer to reference original serializer
1313
serializer.class_attribute :_cache_key # @api private : when present, is first item in cache_key. Ignored if the serializable object defines #cache_key.
14-
serializer.class_attribute :_cache_only # @api private : when fragment caching, whitelists cached_attributes. Cannot combine with except
15-
serializer.class_attribute :_cache_except # @api private : when fragment caching, blacklists cached_attributes. Cannot combine with only
14+
serializer.class_attribute :_cache_only # @api private : when fragment caching, whitelists fetch_attributes. Cannot combine with except
15+
serializer.class_attribute :_cache_except # @api private : when fragment caching, blacklists fetch_attributes. Cannot combine with only
1616
serializer.class_attribute :_cache_options # @api private : used by CachedSerializer, passed to _cache.fetch
1717
# _cache_options include:
1818
# expires_in
@@ -79,13 +79,6 @@ def non_cached_attributes
7979
_attributes - cached_attributes
8080
end
8181

82-
# @api private
83-
# Used by FragmentCache on the CachedSerializer
84-
# to call attribute methods on the fragmented cached serializer.
85-
def fragmented(serializer)
86-
self._fragmented = serializer
87-
end
88-
8982
# Enables a serializer to be automatically cached
9083
#
9184
# Sets +::_cache+ object to <tt>ActionController::Base.cache_store</tt>
@@ -208,46 +201,44 @@ def object_cache_key(serializer, adapter_instance)
208201
end
209202
end
210203

211-
def cached_attributes(fields, cached_attributes, adapter_instance)
204+
### INSTANCE METHODS
205+
def fetch_attributes(fields, cached_attributes, adapter_instance)
212206
if self.class.cache_enabled?
213207
key = cache_key(adapter_instance)
214208
cached_attributes.fetch(key) do
215-
cache_check(adapter_instance) do
209+
self.class.cache_store.fetch(key, self.class._cache_options) do
216210
attributes(fields)
217211
end
218212
end
213+
elsif self.class.fragment_cache_enabled?
214+
fetch_fragment_cache(adapter_instance)
219215
else
220-
cache_check(adapter_instance) do
221-
attributes(fields)
222-
end
216+
attributes(fields)
223217
end
224218
end
225219

226-
def cache_check(adapter_instance)
227-
if self.class.cache_enabled?
228-
self.class.cache_store.fetch(cache_key(adapter_instance), self.class._cache_options) do
220+
def fetch(adapter_instance, cache_options = self.class._cache_options)
221+
if self.class.cache_store
222+
self.class.cache_store.fetch(cache_key(adapter_instance), cache_options) do
229223
yield
230224
end
231-
elsif self.class.fragment_cache_enabled?
232-
fetch_fragment_cache(adapter_instance)
233225
else
234226
yield
235227
end
236228
end
237229

238-
# 1. Create a CachedSerializer and NonCachedSerializer from the serializer class
230+
# 1. Create a CachedSerializer from the serializer class
239231
# 2. Serialize the above two with the given adapter
240232
# 3. Pass their serializations to the adapter +::fragment_cache+
241233
#
242234
# It will split the serializer into two, one that will be cached and one that will not
243235
#
244236
# Given a resource name
245-
# 1. Dynamically creates a CachedSerializer and NonCachedSerializer
237+
# 1. Dynamically creates a CachedSerializer
246238
# for a given class 'name'
247239
# 2. Call
248240
# CachedSerializer.cache(serializer._cache_options)
249-
# CachedSerializer.fragmented(serializer)
250-
# NonCachedSerializer.cache(serializer._cache_options)
241+
# CachedSerializer._fragmented = serializer
251242
# 3. Build a hash keyed to the +cached+ and +non_cached+ serializers
252243
# 4. Call +cached_attributes+ on the serializer class and the above hash
253244
# 5. Return the hash
@@ -256,61 +247,53 @@ def cache_check(adapter_instance)
256247
# When +name+ is <tt>User::Admin</tt>
257248
# creates the Serializer classes (if they don't exist).
258249
# CachedUser_AdminSerializer
259-
# NonCachedUser_AdminSerializer
260250
#
261251
# Given a hash of its cached and non-cached serializers
262252
# 1. Determine cached attributes from serializer class options
263253
# 2. Add cached attributes to cached Serializer
264254
# 3. Add non-cached attributes to non-cached Serializer
265255
def fetch_fragment_cache(adapter_instance)
266-
serializer_class_name = self.class.name.gsub('::'.freeze, '_'.freeze)
267256
self.class._cache_options ||= {}
268257
self.class._cache_options[:key] = self.class._cache_key if self.class._cache_key
269258

270-
cached_serializer = _get_or_create_fragment_cached_serializer(serializer_class_name)
259+
cached_serializer = _get_or_create_fragment_cached_serializer
271260
cached_hash = ActiveModelSerializers::SerializableResource.new(
272261
object,
273262
serializer: cached_serializer,
274263
adapter: adapter_instance.class
275264
).serializable_hash
276265

277-
non_cached_serializer = _get_or_create_fragment_non_cached_serializer(serializer_class_name)
278-
non_cached_hash = ActiveModelSerializers::SerializableResource.new(
279-
object,
280-
serializer: non_cached_serializer,
281-
adapter: adapter_instance.class
282-
).serializable_hash
266+
fields = self.class.non_cached_attributes
267+
non_cached_hash = attributes(fields, true)
283268

284269
# Merge both results
285270
adapter_instance.fragment_cache(cached_hash, non_cached_hash)
286271
end
287272

288-
def _get_or_create_fragment_cached_serializer(serializer_class_name)
289-
cached_serializer = _get_or_create_fragment_serializer "Cached#{serializer_class_name}"
290-
cached_serializer.cache(self.class._cache_options)
291-
cached_serializer.type(self.class._type)
292-
cached_serializer.fragmented(self)
293-
self.class.cached_attributes.each do |attribute|
294-
options = self.class._attributes_keys[attribute] || {}
295-
cached_serializer.attribute(attribute, options)
296-
end
297-
cached_serializer
298-
end
299-
300-
def _get_or_create_fragment_non_cached_serializer(serializer_class_name)
301-
non_cached_serializer = _get_or_create_fragment_serializer "NonCached#{serializer_class_name}"
302-
non_cached_serializer.type(self.class._type)
303-
non_cached_serializer.fragmented(self)
304-
self.class.non_cached_attributes.each do |attribute|
305-
options = self.class._attributes_keys[attribute] || {}
306-
non_cached_serializer.attribute(attribute, options)
273+
def _get_or_create_fragment_cached_serializer
274+
serializer_class_name = self.class.name.gsub('::'.freeze, '_'.freeze)
275+
cached_serializer_name = "Cached#{serializer_class_name}"
276+
if Object.const_defined?(cached_serializer_name)
277+
cached_serializer = Object.const_get(cached_serializer_name)
278+
# HACK: Test concern in production code :(
279+
# But it's better than running all the cached fragment serializer
280+
# code multiple times.
281+
if Rails.env == 'test'.freeze
282+
Object.send(:remove_const, cached_serializer_name)
283+
return _get_or_create_fragment_cached_serializer
284+
end
285+
cached_serializer
286+
else
287+
cached_serializer = Object.const_set(cached_serializer_name, Class.new(ActiveModel::Serializer))
288+
cached_serializer.cache(self.class._cache_options)
289+
cached_serializer.type(self.class._type)
290+
cached_serializer._fragmented = self
291+
self.class.cached_attributes.each do |attribute|
292+
options = self.class._attributes_keys[attribute] || {}
293+
cached_serializer.attribute(attribute, options)
294+
end
295+
cached_serializer
307296
end
308-
non_cached_serializer
309-
end
310-
311-
def _get_or_create_fragment_serializer(name)
312-
return Object.const_get(name) if Object.const_defined?(name)
313-
Object.const_set(name, Class.new(ActiveModel::Serializer))
314297
end
315298

316299
def cache_key(adapter_instance)

lib/active_model_serializers/adapter/json_api.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ def attributes_for(serializer, fields)
294294

295295
# {http://jsonapi.org/format/#document-resource-objects Document Resource Objects}
296296
def resource_object_for(serializer)
297-
resource_object = serializer.cache_check(self) do
297+
resource_object = serializer.fetch(self) do
298298
resource_object = ResourceIdentifier.new(serializer, instance_options).as_json
299299

300300
requested_fields = fieldset && fieldset.fields_for(resource_object[:type])

test/adapter/json_api/resource_identifier_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def test_id_defined_on_serializer
4242
end
4343

4444
def test_id_defined_on_fragmented
45-
FragmentedSerializer.fragmented(WithDefinedIdSerializer.new(@model))
45+
FragmentedSerializer._fragmented = WithDefinedIdSerializer.new(@model)
4646
test_id(FragmentedSerializer, 'special_id')
4747
end
4848

test/cache_test.rb

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ def test_uses_adapter_in_cache_key
204204

205205
# Based on original failing test by @kevintyll
206206
# rubocop:disable Metrics/AbcSize
207-
def test_a_serializer_rendered_by_two_adapter_returns_differently_cached_attributes
207+
def test_a_serializer_rendered_by_two_adapter_returns_differently_fetch_attributes
208208
Object.const_set(:Alert, Class.new(ActiveModelSerializers::Model) do
209209
attr_accessor :id, :status, :resource, :started_at, :ended_at, :updated_at, :created_at
210210
end)
@@ -225,7 +225,7 @@ def test_a_serializer_rendered_by_two_adapter_returns_differently_cached_attribu
225225
created_at: Time.new(2016, 3, 31, 21, 37, 35, 0)
226226
)
227227

228-
expected_cached_attributes = {
228+
expected_fetch_attributes = {
229229
id: 1,
230230
status: 'fail',
231231
resource: 'resource-1',
@@ -250,7 +250,7 @@ def test_a_serializer_rendered_by_two_adapter_returns_differently_cached_attribu
250250
# Assert attributes are serialized correctly
251251
serializable_alert = serializable(alert, serializer: AlertSerializer, adapter: :attributes)
252252
attributes_serialization = serializable_alert.as_json
253-
assert_equal expected_cached_attributes, alert.attributes
253+
assert_equal expected_fetch_attributes, alert.attributes
254254
assert_equal alert.attributes, attributes_serialization
255255
attributes_cache_key = serializable_alert.adapter.serializer.cache_key(serializable_alert.adapter)
256256
assert_equal attributes_serialization, cache_store.fetch(attributes_cache_key)
@@ -296,23 +296,28 @@ def test_object_cache_keys
296296
assert actual.any? { |key| key =~ %r{author/author-\d+} }
297297
end
298298

299-
def test_cached_attributes
300-
serializer = ActiveModel::Serializer::CollectionSerializer.new([@comment, @comment])
299+
def test_fetch_attributes_from_cache
300+
serializers = ActiveModel::Serializer::CollectionSerializer.new([@comment, @comment])
301301

302302
Timecop.freeze(Time.current) do
303303
render_object_with_cache(@comment)
304304

305-
attributes = Adapter::Attributes.new(serializer)
305+
options = {}
306+
adapter_options = {}
307+
adapter_instance = ActiveModelSerializers::Adapter::Attributes.new(serializers, adapter_options)
308+
serializers.serializable_hash(adapter_options, options, adapter_instance)
309+
cached_attributes = adapter_options.fetch(:cached_attributes)
310+
306311
include_directive = ActiveModelSerializers.default_include_directive
307-
cached_attributes = ActiveModel::Serializer.cache_read_multi(serializer, attributes, include_directive)
312+
manual_cached_attributes = ActiveModel::Serializer.cache_read_multi(serializers, adapter_instance, include_directive)
313+
assert_equal manual_cached_attributes, cached_attributes
308314

309-
assert_equal cached_attributes["#{@comment.cache_key}/#{attributes.cache_key}"], Comment.new(id: 1, body: 'ZOMG A COMMENT').attributes
310-
assert_equal cached_attributes["#{@comment.post.cache_key}/#{attributes.cache_key}"], Post.new(id: 'post', title: 'New Post', body: 'Body').attributes
315+
assert_equal cached_attributes["#{@comment.cache_key}/#{adapter_instance.cache_key}"], Comment.new(id: 1, body: 'ZOMG A COMMENT').attributes
316+
assert_equal cached_attributes["#{@comment.post.cache_key}/#{adapter_instance.cache_key}"], Post.new(id: 'post', title: 'New Post', body: 'Body').attributes
311317

312318
writer = @comment.post.blog.writer
313319
writer_cache_key = writer.cache_key
314-
315-
assert_equal cached_attributes["#{writer_cache_key}/#{attributes.cache_key}"], Author.new(id: 'author', name: 'Joao M. D. Moura').attributes
320+
assert_equal cached_attributes["#{writer_cache_key}/#{adapter_instance.cache_key}"], Author.new(id: 'author', name: 'Joao M. D. Moura').attributes
316321
end
317322
end
318323

@@ -442,6 +447,9 @@ def test_fragment_fetch_with_virtual_attributes
442447
name: @role.name
443448
}
444449
assert_equal(@role_hash, expected_result)
450+
ensure
451+
fragmented_serializer = @role_serializer
452+
Object.send(:remove_const, fragmented_serializer._get_or_create_fragment_cached_serializer.name)
445453
end
446454

447455
def test_fragment_fetch_with_namespaced_object
@@ -452,6 +460,9 @@ def test_fragment_fetch_with_namespaced_object
452460
id: @spam.id
453461
}
454462
assert_equal(@spam_hash, expected_result)
463+
ensure
464+
fragmented_serializer = @spam_serializer
465+
Object.send(:remove_const, fragmented_serializer._get_or_create_fragment_cached_serializer.name)
455466
end
456467

457468
private
@@ -476,10 +487,5 @@ def render_object_with_cache(obj, options = {})
476487
def adapter
477488
@serializable_resource.adapter
478489
end
479-
480-
def cached_serialization(serializer)
481-
cache_key = serializer.cache_key(adapter)
482-
cache_store.fetch(cache_key)
483-
end
484490
end
485491
end

0 commit comments

Comments
 (0)