Skip to content

Commit e118599

Browse files
committed
Merge pull request #1642 from bf4/kevintyll-master
[FEATURE] Prefer object.cache_key when available.
2 parents b73b780 + 4ba4c29 commit e118599

File tree

5 files changed

+68
-26
lines changed

5 files changed

+68
-26
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+
- [#1642](https://github.com/rails-api/active_model_serializers/pull/1642) Prefer object.cache_key over the generated
7+
cache key. (@bf4 via #1346 by @kevintyll)
68
- [#1637](https://github.com/rails-api/active_model_serializers/pull/1637) Make references to 'ActionController::Base.cache_store' explicit
79
in order to avoid issues when application controllers inherit from 'ActionController::API'. (@ncuesta)
810
- [#1633](https://github.com/rails-api/active_model_serializers/pull/1633) Yield 'serializer' to serializer association blocks. (@bf4)

lib/active_model/serializer/caching.rb

Lines changed: 1 addition & 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

lib/active_model_serializers/cached_serializer.rb

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
module ActiveModelSerializers
22
class CachedSerializer
3+
UndefinedCacheKey = Class.new(StandardError)
4+
35
def initialize(serializer)
46
@cached_serializer = serializer
57
@klass = @cached_serializer.class
@@ -34,10 +36,18 @@ def cache_key
3436
@cache_key = parts.join('/')
3537
end
3638

39+
# Use object's cache_key if available, else derive a key from the object
40+
# Pass the `key` option to the `cache` declaration or override this method to customize the cache key
3741
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
42+
if @cached_serializer.object.respond_to?(:cache_key)
43+
@cached_serializer.object.cache_key
44+
elsif (cache_key = (@klass._cache_key || @klass._cache_options[:key]))
45+
object_time_safe = @cached_serializer.object.updated_at
46+
object_time_safe = object_time_safe.strftime('%Y%m%d%H%M%S%9N') if object_time_safe.respond_to?(:strftime)
47+
"#{cache_key}/#{@cached_serializer.object.id}-#{object_time_safe}"
48+
else
49+
fail UndefinedCacheKey, "#{@cached_serializer.object.class} must define #cache_key, or the 'key:' option must be passed into '#{@klass}.cache'"
50+
end
4151
end
4252

4353
# find all cache_key for the collection_serializer

test/cache_test.rb

Lines changed: 49 additions & 15 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,27 @@ 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+
def test_cache_key_interpolation_with_updated_at_when_cache_key_is_not_defined_on_object
100+
uncached_author = UncachedAuthor.new(name: 'Joao M. D. Moura')
101+
uncached_author_serializer = AuthorSerializer.new(uncached_author)
102+
103+
render_object_with_cache(uncached_author)
104+
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+
assert_equal(uncached_author_serializer.attributes.to_json, cache_store.fetch(key).to_json)
88106
end
89107

90108
def test_default_cache_key_fallback
91109
render_object_with_cache(@comment)
92-
assert_equal(@comment_serializer.attributes.to_json, cache_store.fetch(@comment.cache_key).to_json)
110+
key = @comment.cache_key
111+
assert_equal(@comment_serializer.attributes.to_json, cache_store.fetch(key).to_json)
112+
end
113+
114+
def test_error_is_raised_if_cache_key_is_not_defined_on_object_or_passed_as_cache_option
115+
article = Article.new(title: 'Must Read')
116+
e = assert_raises ActiveModelSerializers::CachedSerializer::UndefinedCacheKey do
117+
render_object_with_cache(article)
118+
end
119+
assert_match(/ActiveModelSerializers::CacheTest::Article must define #cache_key, or the 'key:' option must be passed into 'CachedActiveModelSerializers_CacheTest_ArticleSerializer.cache'/, e.message)
93120
end
94121

95122
def test_cache_options_definition
@@ -111,8 +138,10 @@ def test_associations_separately_cache
111138
Timecop.freeze(Time.current) do
112139
render_object_with_cache(@post)
113140

114-
assert_equal(@post_serializer.attributes, cache_store.fetch(@post.cache_key))
115-
assert_equal(@comment_serializer.attributes, cache_store.fetch(@comment.cache_key))
141+
key = @post.cache_key
142+
assert_equal(@post_serializer.attributes, cache_store.fetch(key))
143+
key = @comment.cache_key
144+
assert_equal(@comment_serializer.attributes, cache_store.fetch(key))
116145
end
117146
end
118147

@@ -122,8 +151,10 @@ def test_associations_cache_when_updated
122151
render_object_with_cache(@post)
123152

124153
# 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))
154+
key = @post.cache_key
155+
assert_equal(@post_serializer.attributes, cache_store.fetch(key))
156+
key = @comment.cache_key
157+
assert_equal(@comment_serializer.attributes, cache_store.fetch(key))
127158

128159
# Simulating update on comments relationship with Post
129160
new_comment = Comment.new(id: 2567, body: 'ZOMG A NEW COMMENT')
@@ -134,8 +165,10 @@ def test_associations_cache_when_updated
134165
render_object_with_cache(@post)
135166

136167
# 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))
168+
key = new_comment.cache_key
169+
assert_equal(new_comment_serializer.attributes, cache_store.fetch(key))
170+
key = @post.cache_key
171+
assert_equal(@post_serializer.attributes, cache_store.fetch(key))
139172
end
140173
end
141174

@@ -163,11 +196,12 @@ def test_fragment_cache_with_inheritance
163196

164197
def test_uses_file_digest_in_cache_key
165198
render_object_with_cache(@blog)
166-
assert_equal(@blog_serializer.attributes, cache_store.fetch(@blog.cache_key_with_digest))
199+
key = "#{@blog.cache_key}/#{::Model::FILE_DIGEST}"
200+
assert_equal(@blog_serializer.attributes, cache_store.fetch(key))
167201
end
168202

169203
def test_cache_digest_definition
170-
assert_equal(FILE_DIGEST, @post_serializer.class._cache_digest)
204+
assert_equal(::Model::FILE_DIGEST, @post_serializer.class._cache_digest)
171205
end
172206

173207
def test_object_cache_keys
@@ -179,7 +213,7 @@ def test_object_cache_keys
179213
assert_equal actual.size, 3
180214
assert actual.any? { |key| key == 'comment/1' }
181215
assert actual.any? { |key| key =~ %r{post/post-\d+} }
182-
assert actual.any? { |key| key =~ %r{writer/author-\d+} }
216+
assert actual.any? { |key| key =~ %r{author/author-\d+} }
183217
end
184218

185219
def test_cached_attributes
@@ -196,7 +230,7 @@ def test_cached_attributes
196230
assert_equal cached_attributes[@comment.post.cache_key], Post.new(id: 'post', title: 'New Post', body: 'Body').attributes
197231

198232
writer = @comment.post.blog.writer
199-
writer_cache_key = "writer/#{writer.id}-#{writer.updated_at.strftime("%Y%m%d%H%M%S%9N")}"
233+
writer_cache_key = writer.cache_key
200234

201235
assert_equal cached_attributes[writer_cache_key], Author.new(id: 'author', name: 'Joao M. D. Moura').attributes
202236
end

test/fixtures/poro.rb

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
verbose = $VERBOSE
22
$VERBOSE = nil
33
class Model < ActiveModelSerializers::Model
4+
FILE_DIGEST = Digest::MD5.hexdigest(File.open(__FILE__).read)
5+
46
### Helper methods, not required to be serializable
57

68
# Convenience when not adding @attributes readers and writers
@@ -52,13 +54,7 @@ class ProfilePreviewSerializer < ActiveModel::Serializer
5254
Like = Class.new(Model)
5355
Author = Class.new(Model)
5456
Bio = Class.new(Model)
55-
Blog = Class.new(Model) do
56-
FILE_DIGEST = Digest::MD5.hexdigest(File.open(__FILE__).read)
57-
58-
def cache_key_with_digest
59-
"#{cache_key}/#{FILE_DIGEST}"
60-
end
61-
end
57+
Blog = Class.new(Model)
6258
Role = Class.new(Model)
6359
User = Class.new(Model)
6460
Location = Class.new(Model)

0 commit comments

Comments
 (0)