Skip to content

Commit ab6bd60

Browse files
kevintyllbf4
authored andcommitted
When caching, return the object's cache_key up front if it's defined.
This will prevent objects PORO objects that don't have updated_at defined, from throwing an error. Not as big a deal now that PORO objects can inherit ActiveModelSerializers::Model, but still necessary if it's not inherited for whatever reason. Add the Adapter type to the cache key. This prevents incorrect results when the same object is serialized with different adapters. BF: Cherry-pick of bf4@040a97b which was a squash of https://github.com/rails-api/active_model_serializers/commits/f89ed71058322fe7dd35d5c8b209856f8e03ad14 from pr 1346
1 parent b73b780 commit ab6bd60

File tree

5 files changed

+91
-28
lines changed

5 files changed

+91
-28
lines changed

lib/active_model/serializer/caching.rb

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module Caching
77
with_options instance_writer: false, instance_reader: false do |serializer|
88
serializer.class_attribute :_cache # @api private : the cache store
99
serializer.class_attribute :_fragmented # @api private : @see ::fragmented
10-
serializer.class_attribute :_cache_key # @api private : when present, is first item in cache_key
10+
serializer.class_attribute :_cache_key # @api private : when present, is first item in cache_key. Ignored if the serializable object defines #cache_key.
1111
serializer.class_attribute :_cache_only # @api private : when fragment caching, whitelists cached_attributes. Cannot combine with except
1212
serializer.class_attribute :_cache_except # @api private : when fragment caching, blacklists cached_attributes. Cannot combine with only
1313
serializer.class_attribute :_cache_options # @api private : used by CachedSerializer, passed to _cache.fetch
@@ -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.
@@ -142,6 +146,18 @@ def fragment_cache_enabled?
142146
(_cache_only && !_cache_except || !_cache_only && _cache_except)
143147
end
144148
end
149+
150+
# Use object's cache_key if available, else derive a key from the object
151+
# Pass the `key` option to the `cache` declaration or override this method to customize the cache key
152+
def cache_key
153+
if object.respond_to?(:cache_key)
154+
object.cache_key
155+
else
156+
object_time_safe = object.updated_at
157+
object_time_safe = object_time_safe.strftime('%Y%m%d%H%M%S%9N') if object_time_safe.respond_to?(:strftime)
158+
"#{self.class._cache_key}/#{object.id}-#{object_time_safe}"
159+
end
160+
end
145161
end
146162
end
147163
end

lib/active_model_serializers/adapter/base.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,21 @@ def self.inherited(subclass)
88
ActiveModelSerializers::Adapter.register(subclass)
99
end
1010

11+
def self.name
12+
to_s.demodulize
13+
end
14+
1115
attr_reader :serializer, :instance_options
1216

1317
def initialize(serializer, options = {})
1418
@serializer = serializer
1519
@instance_options = options
1620
end
1721

22+
def name
23+
self.class.name
24+
end
25+
1826
def serializable_hash(_options = nil)
1927
fail NotImplementedError, 'This is an abstract method. Should be implemented at the concrete adapter.'
2028
end

lib/active_model_serializers/cached_serializer.rb

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
module ActiveModelSerializers
22
class CachedSerializer
3+
UndefinedCacheKey = Class.new(StandardError)
4+
35
def initialize(serializer)
46
@cached_serializer = serializer
5-
@klass = @cached_serializer.class
7+
return unless cached? && !@cached_serializer.object.respond_to?(:cache_key) && @klass._cache_key.blank?
8+
fail(UndefinedCacheKey, "#{@cached_serializer.object} must define #cache_key, or the cache_key option must be passed into cache on #{@cached_serializer}")
69
end
710

811
def cache_check(adapter_instance)
912
if cached?
10-
@klass._cache.fetch(cache_key, @klass._cache_options) do
13+
@klass._cache.fetch(cache_key(adapter_instance), @klass._cache_options) do
1114
yield
1215
end
1316
elsif fragment_cached?
@@ -25,21 +28,16 @@ def fragment_cached?
2528
@klass.fragment_cache_enabled?
2629
end
2730

28-
def cache_key
31+
def cache_key(adapter_instance)
2932
return @cache_key if defined?(@cache_key)
3033

3134
parts = []
32-
parts << object_cache_key
33-
parts << @klass._cache_digest unless @klass._cache_options && @klass._cache_options[:skip_digest]
35+
parts << @cached_serializer.cache_key
36+
parts << adapter_instance.name.underscore
37+
parts << @klass._cache_digest unless @klass._skip_digest?
3438
@cache_key = parts.join('/')
3539
end
3640

37-
def object_cache_key
38-
object_time_safe = @cached_serializer.object.updated_at
39-
object_time_safe = object_time_safe.strftime('%Y%m%d%H%M%S%9N') if object_time_safe.respond_to?(:strftime)
40-
@klass._cache_key ? "#{@klass._cache_key}/#{@cached_serializer.object.id}-#{object_time_safe}" : @cached_serializer.object.cache_key
41-
end
42-
4341
# find all cache_key for the collection_serializer
4442
# @param collection_serializer
4543
# @param include_tree

test/cache_test.rb

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@
44

55
module ActiveModelSerializers
66
class CacheTest < ActiveSupport::TestCase
7+
UncachedAuthor = Class.new(Author) do
8+
# To confirm cache_key is set using updated_at and cache_key option passed to cache
9+
undef_method :cache_key
10+
end
11+
12+
Article = Class.new(::Model) do
13+
# To confirm error is raised when cache_key is not set and cache_key option not passed to cache
14+
undef_method :cache_key
15+
end
16+
17+
ArticleSerializer = Class.new(ActiveModel::Serializer) do
18+
cache only: [:place], skip_digest: true
19+
attributes :title
20+
end
21+
722
InheritedRoleSerializer = Class.new(RoleSerializer) do
823
cache key: 'inherited_role', only: [:name, :special_attribute]
924
attribute :special_attribute
@@ -81,15 +96,26 @@ def test_cache_key_definition
8196
assert_equal(nil, @comment_serializer.class._cache_key)
8297
end
8398

84-
def test_cache_key_interpolation_with_updated_at
85-
render_object_with_cache(@author)
86-
assert_equal(nil, cache_store.fetch(@author.cache_key))
87-
assert_equal(@author_serializer.attributes.to_json, cache_store.fetch("#{@author_serializer.class._cache_key}/#{@author_serializer.object.id}-#{@author_serializer.object.updated_at.strftime("%Y%m%d%H%M%S%9N")}").to_json)
99+
100+
def test_error_is_raised_if_cache_key_is_not_defined_on_object_or_passed_as_cache_option
101+
article = Article.new(title: 'Must Read')
102+
assert_raises ActiveModel::Serializer::Adapter::CachedSerializer::UndefinedCacheKey do
103+
render_object_with_cache(article)
104+
end
105+
end
106+
107+
def test_cache_key_interpolation_with_updated_at_when_cache_key_is_not_defined_on_object
108+
uncached_author = UncachedAuthor.new(name: 'Joao M. D. Moura')
109+
uncached_author_serializer = AuthorSerializer.new(uncached_author)
110+
111+
render_object_with_cache(uncached_author)
112+
key = cache_key_with_adapter("#{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")}")
113+
assert_equal(uncached_author_serializer.attributes.to_json, ActionController::Base.cache_store.fetch(key).to_json)
88114
end
89115

90116
def test_default_cache_key_fallback
91117
render_object_with_cache(@comment)
92-
assert_equal(@comment_serializer.attributes.to_json, cache_store.fetch(@comment.cache_key).to_json)
118+
assert_equal(@comment_serializer.attributes.to_json, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@comment.cache_key)).to_json)
93119
end
94120

95121
def test_cache_options_definition
@@ -111,8 +137,8 @@ def test_associations_separately_cache
111137
Timecop.freeze(Time.current) do
112138
render_object_with_cache(@post)
113139

114-
assert_equal(@post_serializer.attributes, cache_store.fetch(@post.cache_key))
115-
assert_equal(@comment_serializer.attributes, cache_store.fetch(@comment.cache_key))
140+
assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@post.cache_key)))
141+
assert_equal(@comment_serializer.attributes, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@comment.cache_key)))
116142
end
117143
end
118144

@@ -122,8 +148,9 @@ def test_associations_cache_when_updated
122148
render_object_with_cache(@post)
123149

124150
# Check if it cached the objects separately
125-
assert_equal(@post_serializer.attributes, cached_serialization(@post_serializer))
126-
assert_equal(@comment_serializer.attributes, cached_serialization(@comment_serializer))
151+
assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@post.cache_key)))
152+
assert_equal(@comment_serializer.attributes, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@comment.cache_key)))
153+
127154

128155
# Simulating update on comments relationship with Post
129156
new_comment = Comment.new(id: 2567, body: 'ZOMG A NEW COMMENT')
@@ -134,8 +161,8 @@ def test_associations_cache_when_updated
134161
render_object_with_cache(@post)
135162

136163
# Check if the the new comment was cached
137-
assert_equal(new_comment_serializer.attributes, cached_serialization(new_comment_serializer))
138-
assert_equal(@post_serializer.attributes, cached_serialization(@post_serializer))
164+
assert_equal(new_comment_serializer.attributes, ActionController::Base.cache_store.fetch(cache_key_with_adapter(new_comment.cache_key)))
165+
assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@post.cache_key)))
139166
end
140167
end
141168

@@ -150,7 +177,7 @@ def test_fragment_fetch_with_virtual_associations
150177
hash = render_object_with_cache(@location)
151178

152179
assert_equal(hash, expected_result)
153-
assert_equal({ place: 'Nowhere' }, cache_store.fetch(@location.cache_key))
180+
assert_equal({ place: 'Nowhere' }, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@location.cache_key)))
154181
end
155182

156183
def test_fragment_cache_with_inheritance
@@ -161,9 +188,14 @@ def test_fragment_cache_with_inheritance
161188
refute_includes(base.keys, :special_attribute)
162189
end
163190

191+
def test_uses_adapter_in_cache_key
192+
render_object_with_cache(@post)
193+
assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch("#{@post.cache_key}/#{adapter.class.to_s.demodulize.underscore}"))
194+
end
195+
164196
def test_uses_file_digest_in_cache_key
165197
render_object_with_cache(@blog)
166-
assert_equal(@blog_serializer.attributes, cache_store.fetch(@blog.cache_key_with_digest))
198+
assert_equal(@blog_serializer.attributes, ActionController::Base.cache_store.fetch("#{cache_key_with_adapter(@blog.cache_key)}/#{@blog.class::FILE_DIGEST}"))
167199
end
168200

169201
def test_cache_digest_definition
@@ -254,7 +286,16 @@ def test_warn_on_serializer_not_defined_in_file
254286
private
255287

256288
def render_object_with_cache(obj, options = {})
257-
serializable(obj, options).serializable_hash
289+
@serializable_resource = serializable(obj, options).serializable_hash
290+
@serializable_resource.serializable_hash
291+
end
292+
293+
def adapter
294+
@serializable_resource.adapter
295+
end
296+
297+
def cache_key_with_adapter(key)
298+
"#{key}/#{adapter.name.underscore}"
258299
end
259300

260301
def cache_store

test/fixtures/poro.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ class ProfilePreviewSerializer < ActiveModel::Serializer
5555
Blog = Class.new(Model) do
5656
FILE_DIGEST = Digest::MD5.hexdigest(File.open(__FILE__).read)
5757

58-
def cache_key_with_digest
59-
"#{cache_key}/#{FILE_DIGEST}"
58+
def digest
59+
FILE_DIGEST
6060
end
6161
end
6262
Role = Class.new(Model)

0 commit comments

Comments
 (0)