Skip to content

Commit a2023b9

Browse files
committed
Adding Fragment Cache to AMS
It's an upgrade based on the new Cache implementation rails-api#693. It allows to use the Rails conventions to cache specific attributes or associations. It's based on the Cache Composition implementation.
1 parent 48ed7cf commit a2023b9

File tree

19 files changed

+555
-102
lines changed

19 files changed

+555
-102
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@
44
* adds method to override association [adcb99e, @kurko]
55
* adds `has_one` attribute for backwards compatibility [@ggordon]
66
* updates JSON API support to RC3 [@mateomurphy]
7+
* adds fragment cache support [@joaomdmoura]
8+
* adds cache support to attributes and associations [@joaomdmoura]

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,10 @@ The options are the same options of ```ActiveSupport::Cache::Store```, plus
271271
a ```key``` option that will be the prefix of the object cache
272272
on a pattern ```"#{key}/#{object.id}-#{object.updated_at}"```.
273273

274+
The cache support is optimized to use the cached object in multiple request. An object cached on an ```show``` request will be reused at the ```index```. If there is a relationship with another cached serializer it will also be created and reused automatically.
275+
274276
**[NOTE] Every object is individually cached.**
277+
275278
**[NOTE] The cache is automatically expired after update an object but it's not deleted.**
276279

277280
```ruby
@@ -295,6 +298,27 @@ On this example every ```Post``` object will be cached with
295298
the key ```"post/#{post.id}-#{post.updated_at}"```. You can use this key to expire it as you want,
296299
but in this case it will be automatically expired after 3 hours.
297300

301+
### Fragmenting Caching
302+
303+
If there is some API endpoint that shouldn't be fully cached, you can still optmise it, using Fragment Cache on the attributes and relationships that you want to cache.
304+
305+
You can define the attribute by using ```only``` or ```except``` option on cache method.
306+
307+
**[NOTE] Cache serializers will be used at their relationships**
308+
309+
Example:
310+
311+
```ruby
312+
class PostSerializer < ActiveModel::Serializer
313+
cache key: 'post', expires_in: 3.hours, only: [:title]
314+
attributes :title, :body
315+
316+
has_many :comments
317+
318+
url :post
319+
end
320+
```
321+
298322
## Getting Help
299323

300324
If you find a bug, please report an [Issue](https://github.com/rails-api/active_model_serializers/issues/new).

lib/active_model/serializer.rb

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,41 +10,54 @@ class Serializer
1010

1111
class << self
1212
attr_accessor :_attributes
13+
attr_accessor :_attributes_keys
1314
attr_accessor :_associations
1415
attr_accessor :_urls
1516
attr_accessor :_cache
17+
attr_accessor :_fragmented
1618
attr_accessor :_cache_key
19+
attr_accessor :_cache_only
20+
attr_accessor :_cache_except
1721
attr_accessor :_cache_options
1822
end
1923

2024
def self.inherited(base)
2125
base._attributes = []
26+
base._attributes_keys = {}
2227
base._associations = {}
2328
base._urls = []
2429
end
2530

2631
def self.attributes(*attrs)
32+
attrs = attrs.first if attrs.first.class == Array
2733
@_attributes.concat attrs
2834

2935
attrs.each do |attr|
3036
define_method attr do
3137
object && object.read_attribute_for_serialization(attr)
32-
end unless method_defined?(attr)
38+
end unless method_defined?(attr) || _fragmented.respond_to?(attr)
3339
end
3440
end
3541

3642
def self.attribute(attr, options = {})
3743
key = options.fetch(:key, attr)
44+
@_attributes_keys[attr] = {key: key} if key != attr
3845
@_attributes.concat [key]
3946
define_method key do
4047
object.read_attribute_for_serialization(attr)
41-
end unless method_defined?(key)
48+
end unless method_defined?(key) || _fragmented.respond_to?(attr)
49+
end
50+
51+
def self.fragmented(serializer)
52+
@_fragmented = serializer
4253
end
4354

4455
# Enables a serializer to be automatically cached
4556
def self.cache(options = {})
46-
@_cache = ActionController::Base.cache_store if Rails.configuration.action_controller.perform_caching
47-
@_cache_key = options.delete(:key)
57+
@_cache = ActionController::Base.cache_store if Rails.configuration.action_controller.perform_caching
58+
@_cache_key = options.delete(:key)
59+
@_cache_only = options.delete(:only)
60+
@_cache_except = options.delete(:except)
4861
@_cache_options = (options.empty?) ? nil : options
4962
end
5063

@@ -141,12 +154,12 @@ def self.root_name
141154
attr_accessor :object, :root, :meta, :meta_key, :scope
142155

143156
def initialize(object, options = {})
144-
@object = object
145-
@options = options
146-
@root = options[:root] || (self.class._root ? self.class.root_name : false)
147-
@meta = options[:meta]
148-
@meta_key = options[:meta_key]
149-
@scope = options[:scope]
157+
@object = object
158+
@options = options
159+
@root = options[:root] || (self.class._root ? self.class.root_name : false)
160+
@meta = options[:meta]
161+
@meta_key = options[:meta_key]
162+
@scope = options[:scope]
150163

151164
scope_name = options[:scope_name]
152165
if scope_name && !respond_to?(scope_name)
@@ -183,22 +196,29 @@ def attributes(options = {})
183196
attributes += options[:required_fields] if options[:required_fields]
184197

185198
attributes.each_with_object({}) do |name, hash|
186-
hash[name] = send(name)
199+
unless self.class._fragmented
200+
hash[name] = send(name)
201+
else
202+
hash[name] = self.class._fragmented.public_send(name)
203+
end
187204
end
188205
end
189206

190207
def each_association(&block)
191208
self.class._associations.dup.each do |name, association_options|
192209
next unless object
193-
194210
association_value = send(name)
195211

196212
serializer_class = ActiveModel::Serializer.serializer_for(association_value, association_options)
197213

198-
serializer = serializer_class.new(
199-
association_value,
200-
options.merge(serializer_from_options(association_options))
201-
) if serializer_class
214+
if serializer_class
215+
serializer = serializer_class.new(
216+
association_value,
217+
options.merge(serializer_from_options(association_options))
218+
)
219+
elsif !association_value.nil? && !association_value.instance_of?(Object)
220+
association_options[:association_options][:virtual_value] = association_value
221+
end
202222

203223
if block_given?
204224
block.call(name, serializer, association_options[:association_options])

lib/active_model/serializer/adapter.rb

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'active_model/serializer/adapter/fragment_cache'
2+
13
module ActiveModel
24
class Serializer
35
class Adapter
@@ -32,8 +34,38 @@ def self.adapter_class(adapter)
3234
"ActiveModel::Serializer::Adapter::#{adapter.to_s.classify}".safe_constantize
3335
end
3436

37+
def fragment_cache(*args)
38+
raise NotImplementedError, 'This is an abstract method. Should be implemented at the concrete adapter.'
39+
end
40+
3541
private
3642

43+
def cache_check(serializer)
44+
@serializer = serializer
45+
@klass = serializer.class
46+
if is_cached?
47+
@klass._cache.fetch(cache_key, @klass._cache_options) do
48+
yield
49+
end
50+
elsif is_fragment_cached?
51+
FragmentCache.new(self, @serializer, @options, @root).fetch
52+
else
53+
yield
54+
end
55+
end
56+
57+
def is_cached?
58+
@klass._cache && !@klass._cache_only && !@klass._cache_except
59+
end
60+
61+
def is_fragment_cached?
62+
@klass._cache_only && !@klass._cache_except || !@klass._cache_only && @klass._cache_except
63+
end
64+
65+
def cache_key
66+
(@klass._cache_key) ? "#{@klass._cache_key}/#{@serializer.object.id}-#{@serializer.object.updated_at}" : @serializer.object.cache_key
67+
end
68+
3769
def meta
3870
serializer.meta if serializer.respond_to?(:meta)
3971
end
@@ -50,20 +82,6 @@ def include_meta(json)
5082
json[meta_key] = meta if meta && root
5183
json
5284
end
53-
54-
private
55-
56-
def cached_object
57-
klass = serializer.class
58-
if klass._cache
59-
_cache_key = (klass._cache_key) ? "#{klass._cache_key}/#{serializer.object.id}-#{serializer.object.updated_at}" : serializer.object.cache_key
60-
klass._cache.fetch(_cache_key, klass._cache_options) do
61-
yield
62-
end
63-
else
64-
yield
65-
end
66-
end
6785
end
6886
end
6987
end
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
module ActiveModel
2+
class Serializer
3+
class Adapter
4+
class FragmentCache
5+
6+
attr_reader :serializer
7+
8+
def initialize(adapter, serializer, options, root)
9+
@root = root
10+
@options = options
11+
@adapter = adapter
12+
@serializer = serializer
13+
end
14+
15+
def fetch
16+
klass = serializer.class
17+
# It will split the serializer into two, one that will be cached and other wont
18+
serializers = fragment_serializer(serializer.object.class.name, klass)
19+
20+
# Instanciate both serializers
21+
cached_serializer = serializers[:cached].constantize.new(serializer.object)
22+
non_cached_serializer = serializers[:non_cached].constantize.new(serializer.object)
23+
24+
cached_adapter = @adapter.class.new(cached_serializer, @options)
25+
non_cached_adapter = @adapter.class.new(non_cached_serializer, @options)
26+
27+
# Get serializable hash from both
28+
cached_hash = cached_adapter.serializable_hash
29+
non_cached_hash = non_cached_adapter.serializable_hash
30+
31+
# Merge both results
32+
@adapter.fragment_cache(cached_hash, non_cached_hash)
33+
end
34+
35+
private
36+
37+
def cached_attributes(klass, serializers)
38+
cached_attributes = (klass._cache_only) ? klass._cache_only : serializer.attributes.keys.delete_if {|attr| klass._cache_except.include?(attr) }
39+
non_cached_attributes = serializer.attributes.keys.delete_if {|attr| cached_attributes.include?(attr) }
40+
41+
cached_attributes.each do |attribute|
42+
options = serializer.class._attributes_keys[attribute]
43+
options ||= {}
44+
# Add cached attributes to cached Serializer
45+
serializers[:cached].constantize.attribute(attribute, options)
46+
end
47+
48+
non_cached_attributes.each do |attribute|
49+
options = serializer.class._attributes_keys[attribute]
50+
options ||= {}
51+
# Add non-cached attributes to non-cached Serializer
52+
serializers[:non_cached].constantize.attribute(attribute, options)
53+
end
54+
end
55+
56+
def fragment_serializer(name, klass)
57+
cached = "#{name.capitalize}CachedSerializer"
58+
non_cached = "#{name.capitalize}NonCachedSerializer"
59+
60+
Object.const_set cached, Class.new(ActiveModel::Serializer) unless Object.const_defined?(cached)
61+
Object.const_set non_cached, Class.new(ActiveModel::Serializer) unless Object.const_defined?(non_cached)
62+
63+
klass._cache_options ||= {}
64+
klass._cache_options[:key] = klass._cache_key if klass._cache_key
65+
66+
cached.constantize.cache(klass._cache_options)
67+
68+
cached.constantize.fragmented(serializer)
69+
non_cached.constantize.fragmented(serializer)
70+
71+
serializers = {cached: cached, non_cached: non_cached}
72+
cached_attributes(klass, serializers)
73+
serializers
74+
end
75+
end
76+
end
77+
end
78+
end
Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'active_model/serializer/adapter/json/fragment_cache'
2+
13
module ActiveModel
24
class Serializer
35
class Adapter
@@ -6,31 +8,45 @@ def serializable_hash(options = {})
68
if serializer.respond_to?(:each)
79
@result = serializer.map{|s| self.class.new(s).serializable_hash }
810
else
9-
@result = cached_object do
10-
@hash = serializer.attributes(options)
11-
serializer.each_association do |name, association, opts|
12-
if association.respond_to?(:each)
13-
array_serializer = association
14-
@hash[name] = array_serializer.map { |item| item.attributes(opts) }
15-
else
16-
if association
17-
@hash[name] = association.attributes(options)
18-
else
19-
@hash[name] = nil
11+
@hash = {}
12+
13+
@core = cache_check(serializer) do
14+
serializer.attributes(options)
15+
end
16+
17+
serializer.each_association do |name, association, opts|
18+
if association.respond_to?(:each)
19+
array_serializer = association
20+
@hash[name] = array_serializer.map do |item|
21+
cache_check(item) do
22+
item.attributes(opts)
2023
end
2124
end
25+
else
26+
if association
27+
@hash[name] = cache_check(association) do
28+
association.attributes(options)
29+
end
30+
elsif opts[:virtual_value]
31+
@hash[name] = opts[:virtual_value]
32+
else
33+
@hash[name] = nil
34+
end
2235
end
23-
@hash
2436
end
37+
@result = @core.merge @hash
2538
end
2639

2740
if root = options.fetch(:root, serializer.json_key)
2841
@result = { root => @result }
2942
end
30-
3143
@result
3244
end
3345
end
46+
47+
def fragment_cache(cached_hash, non_cached_hash)
48+
Json::FragmentCache.new().fragment_cache(cached_hash, non_cached_hash)
49+
end
3450
end
3551
end
36-
end
52+
end
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module ActiveModel
2+
class Serializer
3+
class Adapter
4+
class Json < Adapter
5+
class FragmentCache
6+
7+
def fragment_cache(cached_hash, non_cached_hash)
8+
non_cached_hash.merge cached_hash
9+
end
10+
11+
end
12+
end
13+
end
14+
end
15+
end

0 commit comments

Comments
 (0)