Adding Fragment Cache to AMS

It's an upgrade based on the new Cache implementation #693.
It allows to use the Rails conventions to cache
specific attributes or associations.
It's based on the Cache Composition implementation.
This commit is contained in:
João Moura
2015-02-03 21:57:02 -02:00
parent 48ed7cf9ba
commit 792fb8a905
19 changed files with 554 additions and 101 deletions

View File

@@ -1,3 +1,5 @@
require 'active_model/serializer/adapter/fragment_cache'
module ActiveModel
class Serializer
class Adapter
@@ -32,8 +34,38 @@ module ActiveModel
"ActiveModel::Serializer::Adapter::#{adapter.to_s.classify}".safe_constantize
end
def fragment_cache(*args)
raise NotImplementedError, 'This is an abstract method. Should be implemented at the concrete adapter.'
end
private
def cache_check(serializer)
@cached_serializer = serializer
@klass = @cached_serializer.class
if is_cached?
@klass._cache.fetch(cache_key, @klass._cache_options) do
yield
end
elsif is_fragment_cached?
FragmentCache.new(self, @cached_serializer, @options, @root).fetch
else
yield
end
end
def is_cached?
@klass._cache && !@klass._cache_only && !@klass._cache_except
end
def is_fragment_cached?
@klass._cache_only && !@klass._cache_except || !@klass._cache_only && @klass._cache_except
end
def cache_key
(@klass._cache_key) ? "#{@klass._cache_key}/#{@cached_serializer.object.id}-#{@cached_serializer.object.updated_at}" : @cached_serializer.object.cache_key
end
def meta
serializer.meta if serializer.respond_to?(:meta)
end
@@ -50,20 +82,6 @@ module ActiveModel
json[meta_key] = meta if meta && root
json
end
private
def cached_object
klass = serializer.class
if klass._cache
_cache_key = (klass._cache_key) ? "#{klass._cache_key}/#{serializer.object.id}-#{serializer.object.updated_at}" : serializer.object.cache_key
klass._cache.fetch(_cache_key, klass._cache_options) do
yield
end
else
yield
end
end
end
end
end

View File

@@ -0,0 +1,78 @@
module ActiveModel
class Serializer
class Adapter
class FragmentCache
attr_reader :serializer
def initialize(adapter, serializer, options, root)
@root = root
@options = options
@adapter = adapter
@serializer = serializer
end
def fetch
klass = serializer.class
# It will split the serializer into two, one that will be cached and other wont
serializers = fragment_serializer(serializer.object.class.name, klass)
# Instanciate both serializers
cached_serializer = serializers[:cached].constantize.new(serializer.object)
non_cached_serializer = serializers[:non_cached].constantize.new(serializer.object)
cached_adapter = @adapter.class.new(cached_serializer, @options)
non_cached_adapter = @adapter.class.new(non_cached_serializer, @options)
# Get serializable hash from both
cached_hash = cached_adapter.serializable_hash
non_cached_hash = non_cached_adapter.serializable_hash
# Merge both results
@adapter.fragment_cache(cached_hash, non_cached_hash)
end
private
def cached_attributes(klass, serializers)
cached_attributes = (klass._cache_only) ? klass._cache_only : serializer.attributes.keys.delete_if {|attr| klass._cache_except.include?(attr) }
non_cached_attributes = serializer.attributes.keys.delete_if {|attr| cached_attributes.include?(attr) }
cached_attributes.each do |attribute|
options = serializer.class._attributes_keys[attribute]
options ||= {}
# Add cached attributes to cached Serializer
serializers[:cached].constantize.attribute(attribute, options)
end
non_cached_attributes.each do |attribute|
options = serializer.class._attributes_keys[attribute]
options ||= {}
# Add non-cached attributes to non-cached Serializer
serializers[:non_cached].constantize.attribute(attribute, options)
end
end
def fragment_serializer(name, klass)
cached = "#{name.capitalize}CachedSerializer"
non_cached = "#{name.capitalize}NonCachedSerializer"
Object.const_set cached, Class.new(ActiveModel::Serializer) unless Object.const_defined?(cached)
Object.const_set non_cached, Class.new(ActiveModel::Serializer) unless Object.const_defined?(non_cached)
klass._cache_options ||= {}
klass._cache_options[:key] = klass._cache_key if klass._cache_key
cached.constantize.cache(klass._cache_options)
cached.constantize.fragmented(serializer)
non_cached.constantize.fragmented(serializer)
serializers = {cached: cached, non_cached: non_cached}
cached_attributes(klass, serializers)
serializers
end
end
end
end
end

View File

@@ -1,3 +1,5 @@
require 'active_model/serializer/adapter/json/fragment_cache'
module ActiveModel
class Serializer
class Adapter
@@ -6,31 +8,45 @@ module ActiveModel
if serializer.respond_to?(:each)
@result = serializer.map{|s| self.class.new(s).serializable_hash }
else
@result = cached_object do
@hash = serializer.attributes(options)
serializer.each_association do |name, association, opts|
if association.respond_to?(:each)
array_serializer = association
@hash[name] = array_serializer.map { |item| item.attributes(opts) }
else
if association
@hash[name] = association.attributes(options)
else
@hash[name] = nil
@hash = {}
@core = cache_check(serializer) do
serializer.attributes(options)
end
serializer.each_association do |name, association, opts|
if association.respond_to?(:each)
array_serializer = association
@hash[name] = array_serializer.map do |item|
cache_check(item) do
item.attributes(opts)
end
end
else
if association
@hash[name] = cache_check(association) do
association.attributes(options)
end
elsif opts[:virtual_value]
@hash[name] = opts[:virtual_value]
else
@hash[name] = nil
end
end
@hash
end
@result = @core.merge @hash
end
if root = options.fetch(:root, serializer.json_key)
@result = { root => @result }
end
@result
end
end
def fragment_cache(cached_hash, non_cached_hash)
Json::FragmentCache.new().fragment_cache(cached_hash, non_cached_hash)
end
end
end
end
end

View File

@@ -0,0 +1,15 @@
module ActiveModel
class Serializer
class Adapter
class Json < Adapter
class FragmentCache
def fragment_cache(cached_hash, non_cached_hash)
non_cached_hash.merge cached_hash
end
end
end
end
end
end

View File

@@ -1,3 +1,5 @@
require 'active_model/serializer/adapter/json_api/fragment_cache'
module ActiveModel
class Serializer
class Adapter
@@ -26,15 +28,17 @@ module ActiveModel
end
end
else
@hash = cached_object do
@hash[:data] = attributes_for_serializer(serializer, @options)
add_resource_links(@hash[:data], serializer)
@hash
end
@hash[:data] = attributes_for_serializer(serializer, @options)
add_resource_links(@hash[:data], serializer)
end
@hash
end
def fragment_cache(cached_hash, non_cached_hash)
root = false if @options.include?(:include)
JsonApi::FragmentCache.new().fragment_cache(root, cached_hash, non_cached_hash)
end
private
def add_links(resource, name, serializers)
@@ -43,7 +47,7 @@ module ActiveModel
resource[:links][name][:linkage] += serializers.map { |serializer| { type: serializer.type, id: serializer.id.to_s } }
end
def add_link(resource, name, serializer)
def add_link(resource, name, serializer, val=nil)
resource[:links] ||= {}
resource[:links][name] = { linkage: nil }
@@ -76,24 +80,27 @@ module ActiveModel
end
end
def attributes_for_serializer(serializer, options)
if serializer.respond_to?(:each)
result = []
serializer.each do |object|
options[:fields] = @fieldset && @fieldset.fields_for(serializer)
options[:required_fields] = [:id, :type]
attributes = object.attributes(options)
attributes[:id] = attributes[:id].to_s
result << attributes
result << cache_check(object) do
options[:required_fields] = [:id, :type]
attributes = object.attributes(options)
attributes[:id] = attributes[:id].to_s
result << attributes
end
end
else
options[:fields] = @fieldset && @fieldset.fields_for(serializer)
options[:required_fields] = [:id, :type]
result = serializer.attributes(options)
result[:id] = result[:id].to_s
result = cache_check(serializer) do
result = serializer.attributes(options)
result[:id] = result[:id].to_s
result
end
end
result
end
@@ -124,7 +131,11 @@ module ActiveModel
if association.respond_to?(:each)
add_links(attrs, name, association)
else
add_link(attrs, name, association)
if opts[:virtual_value]
add_link(attrs, name, nil, opts[:virtual_value])
else
add_link(attrs, name, association)
end
end
if options[:add_included]

View File

@@ -0,0 +1,22 @@
module ActiveModel
class Serializer
class Adapter
class JsonApi < Adapter
class FragmentCache
def fragment_cache(root, cached_hash, non_cached_hash)
hash = {}
core_cached = cached_hash.first
core_non_cached = non_cached_hash.first
no_root_cache = cached_hash.delete_if {|key, value| key == core_cached[0] }
no_root_non_cache = non_cached_hash.delete_if {|key, value| key == core_non_cached[0] }
cached_resource = (core_cached[1]) ? core_cached[1].merge(core_non_cached[1]) : core_non_cached[1]
hash = (root) ? { root => cached_resource } : cached_resource
hash.merge no_root_non_cache.merge no_root_cache
end
end
end
end
end
end