Skip to content

Commit 96c5516

Browse files
committed
Merge pull request #1644 from bf4/kevintyll-master-cache_serializers_by_adapter
[FIX] Include adapter in cache key so that serialization is per adapter
2 parents e118599 + 16a3f93 commit 96c5516

File tree

6 files changed

+126
-30
lines changed

6 files changed

+126
-30
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
Breaking changes:
44

55
Features:
6+
- [#1644](https://github.com/rails-api/active_model_serializers/pull/1644) Include adapter name in cache key so
7+
that the same serializer can be cached per adapter. (@bf4 via #1346 by @kevintyll)
68
- [#1642](https://github.com/rails-api/active_model_serializers/pull/1642) Prefer object.cache_key over the generated
79
cache key. (@bf4 via #1346 by @kevintyll)
810
- [#1637](https://github.com/rails-api/active_model_serializers/pull/1637) Make references to 'ActionController::Base.cache_store' explicit

lib/active_model/serializer/caching.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ def digest_caller_file(caller_line)
5858
''.freeze
5959
end
6060

61+
def _skip_digest?
62+
_cache_options && _cache_options[:skip_digest]
63+
end
64+
6165
# @api private
6266
# Used by FragmentCache on the CachedSerializer
6367
# to call attribute methods on the fragmented cached serializer.

lib/active_model_serializers/adapter/attributes.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def serializable_hash_for_collection(options)
3030
def cache_read_multi
3131
return {} if ActiveModelSerializers.config.cache_store.blank?
3232

33-
keys = CachedSerializer.object_cache_keys(serializer, @include_tree)
33+
keys = CachedSerializer.object_cache_keys(serializer, self, @include_tree)
3434

3535
return {} if keys.blank?
3636

@@ -49,7 +49,7 @@ def cache_attributes
4949
def cached_attributes(cached_serializer)
5050
return yield unless cached_serializer.cached?
5151

52-
@cached_attributes.fetch(cached_serializer.cache_key) { yield }
52+
@cached_attributes.fetch(cached_serializer.cache_key(self)) { yield }
5353
end
5454

5555
def serializable_hash_for_single_resource(options)

lib/active_model_serializers/adapter/base.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ def initialize(serializer, options = {})
1515
@instance_options = options
1616
end
1717

18+
def cached_name
19+
@cached_name ||= self.class.name.demodulize.underscore
20+
end
21+
1822
def serializable_hash(_options = nil)
1923
fail NotImplementedError, 'This is an abstract method. Should be implemented at the concrete adapter.'
2024
end

lib/active_model_serializers/cached_serializer.rb

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def initialize(serializer)
99

1010
def cache_check(adapter_instance)
1111
if cached?
12-
@klass._cache.fetch(cache_key, @klass._cache_options) do
12+
@klass._cache.fetch(cache_key(adapter_instance), @klass._cache_options) do
1313
yield
1414
end
1515
elsif fragment_cached?
@@ -27,12 +27,13 @@ def fragment_cached?
2727
@klass.fragment_cache_enabled?
2828
end
2929

30-
def cache_key
30+
def cache_key(adapter_instance)
3131
return @cache_key if defined?(@cache_key)
3232

3333
parts = []
3434
parts << object_cache_key
35-
parts << @klass._cache_digest unless @klass._cache_options && @klass._cache_options[:skip_digest]
35+
parts << adapter_instance.cached_name
36+
parts << @klass._cache_digest unless @klass._skip_digest?
3637
@cache_key = parts.join('/')
3738
end
3839

@@ -54,19 +55,19 @@ def object_cache_key
5455
# @param collection_serializer
5556
# @param include_tree
5657
# @return [Array] all cache_key of collection_serializer
57-
def self.object_cache_keys(serializers, include_tree)
58+
def self.object_cache_keys(serializers, adapter_instance, include_tree)
5859
cache_keys = []
5960

6061
serializers.each do |serializer|
61-
cache_keys << object_cache_key(serializer)
62+
cache_keys << object_cache_key(serializer, adapter_instance)
6263

6364
serializer.associations(include_tree).each do |association|
6465
if association.serializer.respond_to?(:each)
6566
association.serializer.each do |sub_serializer|
66-
cache_keys << object_cache_key(sub_serializer)
67+
cache_keys << object_cache_key(sub_serializer, adapter_instance)
6768
end
6869
else
69-
cache_keys << object_cache_key(association.serializer)
70+
cache_keys << object_cache_key(association.serializer, adapter_instance)
7071
end
7172
end
7273
end
@@ -75,11 +76,11 @@ def self.object_cache_keys(serializers, include_tree)
7576
end
7677

7778
# @return [String, nil] the cache_key of the serializer or nil
78-
def self.object_cache_key(serializer)
79+
def self.object_cache_key(serializer, adapter_instance)
7980
return unless serializer.present? && serializer.object.present?
8081

8182
cached_serializer = new(serializer)
82-
cached_serializer.cached? ? cached_serializer.cache_key : nil
83+
cached_serializer.cached? ? cached_serializer.cache_key(adapter_instance) : nil
8384
end
8485
end
8586
end

test/cache_test.rb

Lines changed: 104 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,13 @@ def test_cache_key_interpolation_with_updated_at_when_cache_key_is_not_defined_o
102102

103103
render_object_with_cache(uncached_author)
104104
key = "#{uncached_author_serializer.class._cache_key}/#{uncached_author_serializer.object.id}-#{uncached_author_serializer.object.updated_at.strftime("%Y%m%d%H%M%S%9N")}"
105+
key = "#{key}/#{adapter.cached_name}"
105106
assert_equal(uncached_author_serializer.attributes.to_json, cache_store.fetch(key).to_json)
106107
end
107108

108109
def test_default_cache_key_fallback
109110
render_object_with_cache(@comment)
110-
key = @comment.cache_key
111+
key = "#{@comment.cache_key}/#{adapter.cached_name}"
111112
assert_equal(@comment_serializer.attributes.to_json, cache_store.fetch(key).to_json)
112113
end
113114

@@ -138,9 +139,9 @@ def test_associations_separately_cache
138139
Timecop.freeze(Time.current) do
139140
render_object_with_cache(@post)
140141

141-
key = @post.cache_key
142+
key = "#{@post.cache_key}/#{adapter.cached_name}"
142143
assert_equal(@post_serializer.attributes, cache_store.fetch(key))
143-
key = @comment.cache_key
144+
key = "#{@comment.cache_key}/#{adapter.cached_name}"
144145
assert_equal(@comment_serializer.attributes, cache_store.fetch(key))
145146
end
146147
end
@@ -151,9 +152,9 @@ def test_associations_cache_when_updated
151152
render_object_with_cache(@post)
152153

153154
# Check if it cached the objects separately
154-
key = @post.cache_key
155+
key = "#{@post.cache_key}/#{adapter.cached_name}"
155156
assert_equal(@post_serializer.attributes, cache_store.fetch(key))
156-
key = @comment.cache_key
157+
key = "#{@comment.cache_key}/#{adapter.cached_name}"
157158
assert_equal(@comment_serializer.attributes, cache_store.fetch(key))
158159

159160
# Simulating update on comments relationship with Post
@@ -165,9 +166,9 @@ def test_associations_cache_when_updated
165166
render_object_with_cache(@post)
166167

167168
# Check if the the new comment was cached
168-
key = new_comment.cache_key
169+
key = "#{new_comment.cache_key}/#{adapter.cached_name}"
169170
assert_equal(new_comment_serializer.attributes, cache_store.fetch(key))
170-
key = @post.cache_key
171+
key = "#{@post.cache_key}/#{adapter.cached_name}"
171172
assert_equal(@post_serializer.attributes, cache_store.fetch(key))
172173
end
173174
end
@@ -183,7 +184,8 @@ def test_fragment_fetch_with_virtual_associations
183184
hash = render_object_with_cache(@location)
184185

185186
assert_equal(hash, expected_result)
186-
assert_equal({ place: 'Nowhere' }, cache_store.fetch(@location.cache_key))
187+
key = "#{@location.cache_key}/#{adapter.cached_name}"
188+
assert_equal({ place: 'Nowhere' }, cache_store.fetch(key))
187189
end
188190

189191
def test_fragment_cache_with_inheritance
@@ -194,9 +196,87 @@ def test_fragment_cache_with_inheritance
194196
refute_includes(base.keys, :special_attribute)
195197
end
196198

199+
def test_uses_adapter_in_cache_key
200+
render_object_with_cache(@post)
201+
key = "#{@post.cache_key}/#{adapter.class.to_s.demodulize.underscore}"
202+
assert_equal(@post_serializer.attributes, cache_store.fetch(key))
203+
end
204+
205+
# Based on original failing test by @kevintyll
206+
# rubocop:disable Metrics/AbcSize
207+
def test_a_serializer_rendered_by_two_adapter_returns_differently_cached_attributes
208+
Object.const_set(:Alert, Class.new(ActiveModelSerializers::Model) do
209+
attr_accessor :id, :status, :resource, :started_at, :ended_at, :updated_at, :created_at
210+
end)
211+
Object.const_set(:UncachedAlertSerializer, Class.new(ActiveModel::Serializer) do
212+
attributes :id, :status, :resource, :started_at, :ended_at, :updated_at, :created_at
213+
end)
214+
Object.const_set(:AlertSerializer, Class.new(UncachedAlertSerializer) do
215+
cache
216+
end)
217+
218+
alert = Alert.new(
219+
id: 1,
220+
status: 'fail',
221+
resource: 'resource-1',
222+
started_at: Time.new(2016, 3, 31, 21, 36, 35, 0),
223+
ended_at: nil,
224+
updated_at: Time.new(2016, 3, 31, 21, 27, 35, 0),
225+
created_at: Time.new(2016, 3, 31, 21, 37, 35, 0)
226+
)
227+
228+
expected_cached_attributes = {
229+
id: 1,
230+
status: 'fail',
231+
resource: 'resource-1',
232+
started_at: alert.started_at,
233+
ended_at: nil,
234+
updated_at: alert.updated_at,
235+
created_at: alert.created_at
236+
}
237+
expected_cached_jsonapi_attributes = {
238+
id: '1',
239+
type: 'alerts',
240+
attributes: {
241+
status: 'fail',
242+
resource: 'resource-1',
243+
started_at: alert.started_at,
244+
ended_at: nil,
245+
updated_at: alert.updated_at,
246+
created_at: alert.created_at
247+
}
248+
}
249+
250+
# Assert attributes are serialized correctly
251+
serializable_alert = serializable(alert, serializer: AlertSerializer, adapter: :attributes)
252+
attributes_serialization = serializable_alert.as_json
253+
assert_equal expected_cached_attributes, alert.attributes
254+
assert_equal alert.attributes, attributes_serialization
255+
attributes_cache_key = CachedSerializer.new(serializable_alert.adapter.serializer).cache_key(serializable_alert.adapter)
256+
assert_equal attributes_serialization, cache_store.fetch(attributes_cache_key)
257+
258+
serializable_alert = serializable(alert, serializer: AlertSerializer, adapter: :json_api)
259+
jsonapi_cache_key = CachedSerializer.new(serializable_alert.adapter.serializer).cache_key(serializable_alert.adapter)
260+
# Assert cache keys differ
261+
refute_equal attributes_cache_key, jsonapi_cache_key
262+
# Assert (cached) serializations differ
263+
jsonapi_serialization = serializable_alert.as_json
264+
assert_equal alert.status, jsonapi_serialization.fetch(:data).fetch(:attributes).fetch(:status)
265+
serializable_alert = serializable(alert, serializer: UncachedAlertSerializer, adapter: :json_api)
266+
assert_equal serializable_alert.as_json, jsonapi_serialization
267+
268+
cached_serialization = cache_store.fetch(jsonapi_cache_key)
269+
assert_equal expected_cached_jsonapi_attributes, cached_serialization
270+
ensure
271+
Object.send(:remove_const, :Alert)
272+
Object.send(:remove_const, :AlertSerializer)
273+
Object.send(:remove_const, :UncachedAlertSerializer)
274+
end
275+
# rubocop:enable Metrics/AbcSize
276+
197277
def test_uses_file_digest_in_cache_key
198278
render_object_with_cache(@blog)
199-
key = "#{@blog.cache_key}/#{::Model::FILE_DIGEST}"
279+
key = "#{@blog.cache_key}/#{adapter.cached_name}/#{::Model::FILE_DIGEST}"
200280
assert_equal(@blog_serializer.attributes, cache_store.fetch(key))
201281
end
202282

@@ -205,13 +285,13 @@ def test_cache_digest_definition
205285
end
206286

207287
def test_object_cache_keys
208-
serializer = ActiveModel::Serializer::CollectionSerializer.new([@comment, @comment])
288+
serializable = ActiveModelSerializers::SerializableResource.new([@comment, @comment])
209289
include_tree = ActiveModel::Serializer::IncludeTree.from_include_args('*')
210290

211-
actual = CachedSerializer.object_cache_keys(serializer, include_tree)
291+
actual = CachedSerializer.object_cache_keys(serializable.adapter.serializer, serializable.adapter, include_tree)
212292

213293
assert_equal actual.size, 3
214-
assert actual.any? { |key| key == 'comment/1' }
294+
assert actual.any? { |key| key == "comment/1/#{serializable.adapter.cached_name}" }
215295
assert actual.any? { |key| key =~ %r{post/post-\d+} }
216296
assert actual.any? { |key| key =~ %r{author/author-\d+} }
217297
end
@@ -226,13 +306,13 @@ def test_cached_attributes
226306
attributes.send(:cache_attributes)
227307
cached_attributes = attributes.instance_variable_get(:@cached_attributes)
228308

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

232312
writer = @comment.post.blog.writer
233313
writer_cache_key = writer.cache_key
234314

235-
assert_equal cached_attributes[writer_cache_key], Author.new(id: 'author', name: 'Joao M. D. Moura').attributes
315+
assert_equal cached_attributes["#{writer_cache_key}/#{attributes.cached_name}"], Author.new(id: 'author', name: 'Joao M. D. Moura').attributes
236316
end
237317
end
238318

@@ -287,16 +367,21 @@ def test_warn_on_serializer_not_defined_in_file
287367

288368
private
289369

370+
def cache_store
371+
ActiveModelSerializers.config.cache_store
372+
end
373+
290374
def render_object_with_cache(obj, options = {})
291-
serializable(obj, options).serializable_hash
375+
@serializable_resource = serializable(obj, options)
376+
@serializable_resource.serializable_hash
292377
end
293378

294-
def cache_store
295-
ActiveModelSerializers.config.cache_store
379+
def adapter
380+
@serializable_resource.adapter
296381
end
297382

298383
def cached_serialization(serializer)
299-
cache_key = CachedSerializer.new(serializer).cache_key
384+
cache_key = CachedSerializer.new(serializer).cache_key(adapter)
300385
cache_store.fetch(cache_key)
301386
end
302387
end

0 commit comments

Comments
 (0)