Skip to content

Commit 163aa1d

Browse files
committed
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.
1 parent b68d7f4 commit 163aa1d

File tree

18 files changed

+456
-73
lines changed

18 files changed

+456
-73
lines changed

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: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,25 @@ 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
1617
attr_accessor :_cache_key
18+
attr_accessor :_cache_only
19+
attr_accessor :_cache_except
1720
attr_accessor :_cache_options
1821
end
1922

2023
def self.inherited(base)
2124
base._attributes = []
25+
base._attributes_keys = {}
2226
base._associations = {}
2327
base._urls = []
2428
end
2529

2630
def self.attributes(*attrs)
31+
attrs = attrs.first if attrs.first.class == Array
2732
@_attributes.concat attrs
2833

2934
attrs.each do |attr|
@@ -35,6 +40,7 @@ def self.attributes(*attrs)
3540

3641
def self.attribute(attr, options = {})
3742
key = options.fetch(:key, attr)
43+
@_attributes_keys[attr] = {key: key} if key != attr
3844
@_attributes.concat [key]
3945
define_method key do
4046
object.read_attribute_for_serialization(attr)
@@ -43,8 +49,10 @@ def self.attribute(attr, options = {})
4349

4450
# Enables a serializer to be automatically cached
4551
def self.cache(options = {})
46-
@_cache = ActionController::Base.cache_store if Rails.configuration.action_controller.perform_caching
47-
@_cache_key = options.delete(:key)
52+
@_cache = ActionController::Base.cache_store if Rails.configuration.action_controller.perform_caching
53+
@_cache_key = options.delete(:key)
54+
@_cache_only = options.delete(:only)
55+
@_cache_except = options.delete(:except)
4856
@_cache_options = (options.empty?) ? nil : options
4957
end
5058

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: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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_and_association(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+
cached.constantize.cache(klass._cache_options)
66+
67+
serializers = {cached: cached, non_cached: non_cached}
68+
cached_attributes_and_association(klass, serializers)
69+
serializers
70+
end
71+
end
72+
end
73+
end
74+
end
Lines changed: 28 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,43 @@ 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+
else
31+
@hash[name] = nil
32+
end
2233
end
23-
@hash
2434
end
35+
@result = @core.merge @hash
2536
end
2637

2738
if root = options.fetch(:root, serializer.json_key)
2839
@result = { root => @result }
2940
end
30-
3141
@result
3242
end
3343
end
44+
45+
def fragment_cache(cached_hash, non_cached_hash)
46+
Json::FragmentCache.new().fragment_cache(cached_hash, non_cached_hash)
47+
end
3448
end
3549
end
36-
end
50+
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

lib/active_model/serializer/adapter/json_api.rb

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'active_model/serializer/adapter/json_api/fragment_cache'
2+
13
module ActiveModel
24
class Serializer
35
class Adapter
@@ -23,15 +25,17 @@ def serializable_hash(options = {})
2325
self.class.new(s, @options.merge(top: @top, fieldset: @fieldset)).serializable_hash[@root]
2426
end
2527
else
26-
@hash = cached_object do
27-
@hash[@root] = attributes_for_serializer(serializer, @options)
28-
add_resource_links(@hash[@root], serializer)
29-
@hash
30-
end
28+
@hash[@root] = attributes_for_serializer(serializer, @options)
29+
add_resource_links(@hash[@root], serializer)
3130
end
3231
@hash
3332
end
3433

34+
def fragment_cache(cached_hash, non_cached_hash)
35+
root = false if @options.include?(:include)
36+
JsonApi::FragmentCache.new().fragment_cache(root, cached_hash, non_cached_hash)
37+
end
38+
3539
private
3640

3741
def add_links(resource, name, serializers)
@@ -91,22 +95,25 @@ def add_linked(resource_name, serializers, parent = nil)
9195
end
9296
end
9397

94-
9598
def attributes_for_serializer(serializer, options)
9699
if serializer.respond_to?(:each)
97100
result = []
98101
serializer.each do |object|
99102
options[:fields] = @fieldset && @fieldset.fields_for(serializer)
100-
attributes = object.attributes(options)
101-
attributes[:id] = attributes[:id].to_s if attributes[:id]
102-
result << attributes
103+
result << cache_check(object) do
104+
attributes = object.attributes(options)
105+
attributes[:id] = attributes[:id].to_s if attributes[:id]
106+
result << attributes
107+
end
103108
end
104109
else
105110
options[:fields] = @fieldset && @fieldset.fields_for(serializer)
106-
result = serializer.attributes(options)
107-
result[:id] = result[:id].to_s if result[:id]
111+
result = cache_check(serializer) do
112+
result = serializer.attributes(options)
113+
result[:id] = result[:id].to_s if result[:id]
114+
result
115+
end
108116
end
109-
110117
result
111118
end
112119

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
module ActiveModel
2+
class Serializer
3+
class Adapter
4+
class JsonApi < Adapter
5+
class FragmentCache
6+
7+
def fragment_cache(root, cached_hash, non_cached_hash)
8+
hash = {}
9+
core_cached = cached_hash.first
10+
core_non_cached = non_cached_hash.first
11+
no_root_cache = cached_hash.delete_if {|key, value| key == core_cached[0] }
12+
no_root_non_cache = non_cached_hash.delete_if {|key, value| key == core_non_cached[0] }
13+
cached_resource = (core_cached[1]) ? core_cached[1].merge(core_non_cached[1]) : core_non_cached[1]
14+
hash = (root) ? { root => cached_resource } : cached_resource
15+
hash.merge no_root_non_cache.merge no_root_cache
16+
end
17+
18+
end
19+
end
20+
end
21+
end
22+
end

0 commit comments

Comments
 (0)