mirror of
https://github.com/ditkrg/active_model_serializers.git
synced 2026-01-23 06:16:50 +00:00
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 of040a97b9e9which was a squash off89ed71058from pr 1346
This commit is contained in:
parent
b73b780b79
commit
ab6bd600e3
@ -7,7 +7,7 @@ module ActiveModel
|
|||||||
with_options instance_writer: false, instance_reader: false do |serializer|
|
with_options instance_writer: false, instance_reader: false do |serializer|
|
||||||
serializer.class_attribute :_cache # @api private : the cache store
|
serializer.class_attribute :_cache # @api private : the cache store
|
||||||
serializer.class_attribute :_fragmented # @api private : @see ::fragmented
|
serializer.class_attribute :_fragmented # @api private : @see ::fragmented
|
||||||
serializer.class_attribute :_cache_key # @api private : when present, is first item in cache_key
|
serializer.class_attribute :_cache_key # @api private : when present, is first item in cache_key. Ignored if the serializable object defines #cache_key.
|
||||||
serializer.class_attribute :_cache_only # @api private : when fragment caching, whitelists cached_attributes. Cannot combine with except
|
serializer.class_attribute :_cache_only # @api private : when fragment caching, whitelists cached_attributes. Cannot combine with except
|
||||||
serializer.class_attribute :_cache_except # @api private : when fragment caching, blacklists cached_attributes. Cannot combine with only
|
serializer.class_attribute :_cache_except # @api private : when fragment caching, blacklists cached_attributes. Cannot combine with only
|
||||||
serializer.class_attribute :_cache_options # @api private : used by CachedSerializer, passed to _cache.fetch
|
serializer.class_attribute :_cache_options # @api private : used by CachedSerializer, passed to _cache.fetch
|
||||||
@ -58,6 +58,10 @@ module ActiveModel
|
|||||||
''.freeze
|
''.freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def _skip_digest?
|
||||||
|
_cache_options && _cache_options[:skip_digest]
|
||||||
|
end
|
||||||
|
|
||||||
# @api private
|
# @api private
|
||||||
# Used by FragmentCache on the CachedSerializer
|
# Used by FragmentCache on the CachedSerializer
|
||||||
# to call attribute methods on the fragmented cached serializer.
|
# to call attribute methods on the fragmented cached serializer.
|
||||||
@ -142,6 +146,18 @@ module ActiveModel
|
|||||||
(_cache_only && !_cache_except || !_cache_only && _cache_except)
|
(_cache_only && !_cache_except || !_cache_only && _cache_except)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Use object's cache_key if available, else derive a key from the object
|
||||||
|
# Pass the `key` option to the `cache` declaration or override this method to customize the cache key
|
||||||
|
def cache_key
|
||||||
|
if object.respond_to?(:cache_key)
|
||||||
|
object.cache_key
|
||||||
|
else
|
||||||
|
object_time_safe = object.updated_at
|
||||||
|
object_time_safe = object_time_safe.strftime('%Y%m%d%H%M%S%9N') if object_time_safe.respond_to?(:strftime)
|
||||||
|
"#{self.class._cache_key}/#{object.id}-#{object_time_safe}"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -8,6 +8,10 @@ module ActiveModelSerializers
|
|||||||
ActiveModelSerializers::Adapter.register(subclass)
|
ActiveModelSerializers::Adapter.register(subclass)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.name
|
||||||
|
to_s.demodulize
|
||||||
|
end
|
||||||
|
|
||||||
attr_reader :serializer, :instance_options
|
attr_reader :serializer, :instance_options
|
||||||
|
|
||||||
def initialize(serializer, options = {})
|
def initialize(serializer, options = {})
|
||||||
@ -15,6 +19,10 @@ module ActiveModelSerializers
|
|||||||
@instance_options = options
|
@instance_options = options
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def name
|
||||||
|
self.class.name
|
||||||
|
end
|
||||||
|
|
||||||
def serializable_hash(_options = nil)
|
def serializable_hash(_options = nil)
|
||||||
fail NotImplementedError, 'This is an abstract method. Should be implemented at the concrete adapter.'
|
fail NotImplementedError, 'This is an abstract method. Should be implemented at the concrete adapter.'
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
module ActiveModelSerializers
|
module ActiveModelSerializers
|
||||||
class CachedSerializer
|
class CachedSerializer
|
||||||
|
UndefinedCacheKey = Class.new(StandardError)
|
||||||
|
|
||||||
def initialize(serializer)
|
def initialize(serializer)
|
||||||
@cached_serializer = serializer
|
@cached_serializer = serializer
|
||||||
@klass = @cached_serializer.class
|
return unless cached? && !@cached_serializer.object.respond_to?(:cache_key) && @klass._cache_key.blank?
|
||||||
|
fail(UndefinedCacheKey, "#{@cached_serializer.object} must define #cache_key, or the cache_key option must be passed into cache on #{@cached_serializer}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def cache_check(adapter_instance)
|
def cache_check(adapter_instance)
|
||||||
if cached?
|
if cached?
|
||||||
@klass._cache.fetch(cache_key, @klass._cache_options) do
|
@klass._cache.fetch(cache_key(adapter_instance), @klass._cache_options) do
|
||||||
yield
|
yield
|
||||||
end
|
end
|
||||||
elsif fragment_cached?
|
elsif fragment_cached?
|
||||||
@ -25,21 +28,16 @@ module ActiveModelSerializers
|
|||||||
@klass.fragment_cache_enabled?
|
@klass.fragment_cache_enabled?
|
||||||
end
|
end
|
||||||
|
|
||||||
def cache_key
|
def cache_key(adapter_instance)
|
||||||
return @cache_key if defined?(@cache_key)
|
return @cache_key if defined?(@cache_key)
|
||||||
|
|
||||||
parts = []
|
parts = []
|
||||||
parts << object_cache_key
|
parts << @cached_serializer.cache_key
|
||||||
parts << @klass._cache_digest unless @klass._cache_options && @klass._cache_options[:skip_digest]
|
parts << adapter_instance.name.underscore
|
||||||
|
parts << @klass._cache_digest unless @klass._skip_digest?
|
||||||
@cache_key = parts.join('/')
|
@cache_key = parts.join('/')
|
||||||
end
|
end
|
||||||
|
|
||||||
def object_cache_key
|
|
||||||
object_time_safe = @cached_serializer.object.updated_at
|
|
||||||
object_time_safe = object_time_safe.strftime('%Y%m%d%H%M%S%9N') if object_time_safe.respond_to?(:strftime)
|
|
||||||
@klass._cache_key ? "#{@klass._cache_key}/#{@cached_serializer.object.id}-#{object_time_safe}" : @cached_serializer.object.cache_key
|
|
||||||
end
|
|
||||||
|
|
||||||
# find all cache_key for the collection_serializer
|
# find all cache_key for the collection_serializer
|
||||||
# @param collection_serializer
|
# @param collection_serializer
|
||||||
# @param include_tree
|
# @param include_tree
|
||||||
|
|||||||
@ -4,6 +4,21 @@ require 'tempfile'
|
|||||||
|
|
||||||
module ActiveModelSerializers
|
module ActiveModelSerializers
|
||||||
class CacheTest < ActiveSupport::TestCase
|
class CacheTest < ActiveSupport::TestCase
|
||||||
|
UncachedAuthor = Class.new(Author) do
|
||||||
|
# To confirm cache_key is set using updated_at and cache_key option passed to cache
|
||||||
|
undef_method :cache_key
|
||||||
|
end
|
||||||
|
|
||||||
|
Article = Class.new(::Model) do
|
||||||
|
# To confirm error is raised when cache_key is not set and cache_key option not passed to cache
|
||||||
|
undef_method :cache_key
|
||||||
|
end
|
||||||
|
|
||||||
|
ArticleSerializer = Class.new(ActiveModel::Serializer) do
|
||||||
|
cache only: [:place], skip_digest: true
|
||||||
|
attributes :title
|
||||||
|
end
|
||||||
|
|
||||||
InheritedRoleSerializer = Class.new(RoleSerializer) do
|
InheritedRoleSerializer = Class.new(RoleSerializer) do
|
||||||
cache key: 'inherited_role', only: [:name, :special_attribute]
|
cache key: 'inherited_role', only: [:name, :special_attribute]
|
||||||
attribute :special_attribute
|
attribute :special_attribute
|
||||||
@ -81,15 +96,26 @@ module ActiveModelSerializers
|
|||||||
assert_equal(nil, @comment_serializer.class._cache_key)
|
assert_equal(nil, @comment_serializer.class._cache_key)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_cache_key_interpolation_with_updated_at
|
|
||||||
render_object_with_cache(@author)
|
def test_error_is_raised_if_cache_key_is_not_defined_on_object_or_passed_as_cache_option
|
||||||
assert_equal(nil, cache_store.fetch(@author.cache_key))
|
article = Article.new(title: 'Must Read')
|
||||||
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)
|
assert_raises ActiveModel::Serializer::Adapter::CachedSerializer::UndefinedCacheKey do
|
||||||
|
render_object_with_cache(article)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_cache_key_interpolation_with_updated_at_when_cache_key_is_not_defined_on_object
|
||||||
|
uncached_author = UncachedAuthor.new(name: 'Joao M. D. Moura')
|
||||||
|
uncached_author_serializer = AuthorSerializer.new(uncached_author)
|
||||||
|
|
||||||
|
render_object_with_cache(uncached_author)
|
||||||
|
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")}")
|
||||||
|
assert_equal(uncached_author_serializer.attributes.to_json, ActionController::Base.cache_store.fetch(key).to_json)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_default_cache_key_fallback
|
def test_default_cache_key_fallback
|
||||||
render_object_with_cache(@comment)
|
render_object_with_cache(@comment)
|
||||||
assert_equal(@comment_serializer.attributes.to_json, cache_store.fetch(@comment.cache_key).to_json)
|
assert_equal(@comment_serializer.attributes.to_json, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@comment.cache_key)).to_json)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_cache_options_definition
|
def test_cache_options_definition
|
||||||
@ -111,8 +137,8 @@ module ActiveModelSerializers
|
|||||||
Timecop.freeze(Time.current) do
|
Timecop.freeze(Time.current) do
|
||||||
render_object_with_cache(@post)
|
render_object_with_cache(@post)
|
||||||
|
|
||||||
assert_equal(@post_serializer.attributes, cache_store.fetch(@post.cache_key))
|
assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@post.cache_key)))
|
||||||
assert_equal(@comment_serializer.attributes, cache_store.fetch(@comment.cache_key))
|
assert_equal(@comment_serializer.attributes, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@comment.cache_key)))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -122,8 +148,9 @@ module ActiveModelSerializers
|
|||||||
render_object_with_cache(@post)
|
render_object_with_cache(@post)
|
||||||
|
|
||||||
# Check if it cached the objects separately
|
# Check if it cached the objects separately
|
||||||
assert_equal(@post_serializer.attributes, cached_serialization(@post_serializer))
|
assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@post.cache_key)))
|
||||||
assert_equal(@comment_serializer.attributes, cached_serialization(@comment_serializer))
|
assert_equal(@comment_serializer.attributes, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@comment.cache_key)))
|
||||||
|
|
||||||
|
|
||||||
# Simulating update on comments relationship with Post
|
# Simulating update on comments relationship with Post
|
||||||
new_comment = Comment.new(id: 2567, body: 'ZOMG A NEW COMMENT')
|
new_comment = Comment.new(id: 2567, body: 'ZOMG A NEW COMMENT')
|
||||||
@ -134,8 +161,8 @@ module ActiveModelSerializers
|
|||||||
render_object_with_cache(@post)
|
render_object_with_cache(@post)
|
||||||
|
|
||||||
# Check if the the new comment was cached
|
# Check if the the new comment was cached
|
||||||
assert_equal(new_comment_serializer.attributes, cached_serialization(new_comment_serializer))
|
assert_equal(new_comment_serializer.attributes, ActionController::Base.cache_store.fetch(cache_key_with_adapter(new_comment.cache_key)))
|
||||||
assert_equal(@post_serializer.attributes, cached_serialization(@post_serializer))
|
assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@post.cache_key)))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -150,7 +177,7 @@ module ActiveModelSerializers
|
|||||||
hash = render_object_with_cache(@location)
|
hash = render_object_with_cache(@location)
|
||||||
|
|
||||||
assert_equal(hash, expected_result)
|
assert_equal(hash, expected_result)
|
||||||
assert_equal({ place: 'Nowhere' }, cache_store.fetch(@location.cache_key))
|
assert_equal({ place: 'Nowhere' }, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@location.cache_key)))
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_fragment_cache_with_inheritance
|
def test_fragment_cache_with_inheritance
|
||||||
@ -161,9 +188,14 @@ module ActiveModelSerializers
|
|||||||
refute_includes(base.keys, :special_attribute)
|
refute_includes(base.keys, :special_attribute)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_uses_adapter_in_cache_key
|
||||||
|
render_object_with_cache(@post)
|
||||||
|
assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch("#{@post.cache_key}/#{adapter.class.to_s.demodulize.underscore}"))
|
||||||
|
end
|
||||||
|
|
||||||
def test_uses_file_digest_in_cache_key
|
def test_uses_file_digest_in_cache_key
|
||||||
render_object_with_cache(@blog)
|
render_object_with_cache(@blog)
|
||||||
assert_equal(@blog_serializer.attributes, cache_store.fetch(@blog.cache_key_with_digest))
|
assert_equal(@blog_serializer.attributes, ActionController::Base.cache_store.fetch("#{cache_key_with_adapter(@blog.cache_key)}/#{@blog.class::FILE_DIGEST}"))
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_cache_digest_definition
|
def test_cache_digest_definition
|
||||||
@ -254,7 +286,16 @@ module ActiveModelSerializers
|
|||||||
private
|
private
|
||||||
|
|
||||||
def render_object_with_cache(obj, options = {})
|
def render_object_with_cache(obj, options = {})
|
||||||
serializable(obj, options).serializable_hash
|
@serializable_resource = serializable(obj, options).serializable_hash
|
||||||
|
@serializable_resource.serializable_hash
|
||||||
|
end
|
||||||
|
|
||||||
|
def adapter
|
||||||
|
@serializable_resource.adapter
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_key_with_adapter(key)
|
||||||
|
"#{key}/#{adapter.name.underscore}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def cache_store
|
def cache_store
|
||||||
|
|||||||
4
test/fixtures/poro.rb
vendored
4
test/fixtures/poro.rb
vendored
@ -55,8 +55,8 @@ Bio = Class.new(Model)
|
|||||||
Blog = Class.new(Model) do
|
Blog = Class.new(Model) do
|
||||||
FILE_DIGEST = Digest::MD5.hexdigest(File.open(__FILE__).read)
|
FILE_DIGEST = Digest::MD5.hexdigest(File.open(__FILE__).read)
|
||||||
|
|
||||||
def cache_key_with_digest
|
def digest
|
||||||
"#{cache_key}/#{FILE_DIGEST}"
|
FILE_DIGEST
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
Role = Class.new(Model)
|
Role = Class.new(Model)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user