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

@@ -10,41 +10,54 @@ module ActiveModel
class << self
attr_accessor :_attributes
attr_accessor :_attributes_keys
attr_accessor :_associations
attr_accessor :_urls
attr_accessor :_cache
attr_accessor :_fragmented
attr_accessor :_cache_key
attr_accessor :_cache_only
attr_accessor :_cache_except
attr_accessor :_cache_options
end
def self.inherited(base)
base._attributes = []
base._attributes_keys = {}
base._associations = {}
base._urls = []
end
def self.attributes(*attrs)
attrs = attrs.first if attrs.first.class == Array
@_attributes.concat attrs
attrs.each do |attr|
define_method attr do
object && object.read_attribute_for_serialization(attr)
end unless method_defined?(attr)
end unless method_defined?(attr) || _fragmented.respond_to?(attr)
end
end
def self.attribute(attr, options = {})
key = options.fetch(:key, attr)
@_attributes_keys[attr] = {key: key} if key != attr
@_attributes.concat [key]
define_method key do
object.read_attribute_for_serialization(attr)
end unless method_defined?(key)
end unless method_defined?(key) || _fragmented.respond_to?(attr)
end
def self.fragmented(serializer)
@_fragmented = serializer
end
# Enables a serializer to be automatically cached
def self.cache(options = {})
@_cache = ActionController::Base.cache_store if Rails.configuration.action_controller.perform_caching
@_cache_key = options.delete(:key)
@_cache = ActionController::Base.cache_store if Rails.configuration.action_controller.perform_caching
@_cache_key = options.delete(:key)
@_cache_only = options.delete(:only)
@_cache_except = options.delete(:except)
@_cache_options = (options.empty?) ? nil : options
end
@@ -141,12 +154,12 @@ module ActiveModel
attr_accessor :object, :root, :meta, :meta_key, :scope
def initialize(object, options = {})
@object = object
@options = options
@root = options[:root] || (self.class._root ? self.class.root_name : false)
@meta = options[:meta]
@meta_key = options[:meta_key]
@scope = options[:scope]
@object = object
@options = options
@root = options[:root] || (self.class._root ? self.class.root_name : false)
@meta = options[:meta]
@meta_key = options[:meta_key]
@scope = options[:scope]
scope_name = options[:scope_name]
if scope_name && !respond_to?(scope_name)
@@ -183,22 +196,29 @@ module ActiveModel
attributes += options[:required_fields] if options[:required_fields]
attributes.each_with_object({}) do |name, hash|
hash[name] = send(name)
unless self.class._fragmented
hash[name] = send(name)
else
hash[name] = self.class._fragmented.public_send(name)
end
end
end
def each_association(&block)
self.class._associations.dup.each do |name, association_options|
next unless object
association_value = send(name)
serializer_class = ActiveModel::Serializer.serializer_for(association_value, association_options)
serializer = serializer_class.new(
association_value,
options.merge(serializer_from_options(association_options))
) if serializer_class
if serializer_class
serializer = serializer_class.new(
association_value,
options.merge(serializer_from_options(association_options))
)
elsif !association_value.nil? && !association_value.instance_of?(Object)
association_options[:association_options][:virtual_value] = association_value
end
if block_given?
block.call(name, serializer, association_options[:association_options])

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