Skip to content

Commit 4435fd0

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 a824376 commit 4435fd0

File tree

14 files changed

+363
-36
lines changed

14 files changed

+363
-36
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ a ```key``` option that will be the prefix of the object cache
271271
on a pattern ```"#{key}/#{object.id}-#{object.updated_at}"```.
272272

273273
**[NOTE] Every object is individually cached.**
274+
274275
**[NOTE] The cache is automatically expired after update an object but it's not deleted.**
275276

276277
```ruby
@@ -294,6 +295,25 @@ On this example every ```Post``` object will be cached with
294295
the key ```"post/#{post.id}-#{post.updated_at}"```. You can use this key to expire it as you want,
295296
but in this case it will be automatically expired after 3 hours.
296297

298+
### Fragmenting Caching
299+
300+
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.
301+
302+
You can define the attribute or relationships by using ```only``` or ```except``` option on cache method.
303+
304+
Example:
305+
306+
```ruby
307+
class PostSerializer < ActiveModel::Serializer
308+
cache key: 'post', expires_in: 3.hours, only: [:title]
309+
attributes :title, :body
310+
311+
has_many :comments
312+
313+
url :post
314+
end
315+
```
316+
297317
## Getting Help
298318

299319
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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,25 @@ class Serializer
88

99
class << self
1010
attr_accessor :_attributes
11+
attr_accessor :_attributes_keys
1112
attr_accessor :_associations
1213
attr_accessor :_urls
1314
attr_accessor :_cache
1415
attr_accessor :_cache_key
16+
attr_accessor :_cache_only
17+
attr_accessor :_cache_except
1518
attr_accessor :_cache_options
1619
end
1720

1821
def self.inherited(base)
1922
base._attributes = []
23+
base._attributes_keys = {}
2024
base._associations = {}
2125
base._urls = []
2226
end
2327

2428
def self.attributes(*attrs)
29+
attrs = attrs.first if attrs.first.class == Array
2530
@_attributes.concat attrs
2631

2732
attrs.each do |attr|
@@ -33,6 +38,7 @@ def self.attributes(*attrs)
3338

3439
def self.attribute(attr, options = {})
3540
key = options.fetch(:key, attr)
41+
@_attributes_keys[attr] = {key: key} if key != attr
3642
@_attributes.concat [key]
3743
define_method key do
3844
object.read_attribute_for_serialization(attr)
@@ -43,6 +49,8 @@ def self.attribute(attr, options = {})
4349
def self.cache(options = {})
4450
@_cache = ActionController::Base.cache_store if Rails.configuration.action_controller.perform_caching
4551
@_cache_key = options.delete(:key)
52+
@_cache_only = options.delete(:only)
53+
@_cache_except = options.delete(:except)
4654
@_cache_options = (options.empty?) ? nil : options
4755
end
4856

lib/active_model/serializer/adapter.rb

Lines changed: 31 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
@@ -11,6 +13,7 @@ class Adapter
1113
def initialize(serializer, options = {})
1214
@serializer = serializer
1315
@options = options
16+
@klass = serializer.class
1417
end
1518

1619
def serializable_hash(options = {})
@@ -32,8 +35,36 @@ def self.adapter_class(adapter)
3235
"ActiveModel::Serializer::Adapter::#{adapter.to_s.classify}".safe_constantize
3336
end
3437

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

44+
def cache_check
45+
if is_cached?
46+
@klass._cache.fetch(cache_key, @klass._cache_options) do
47+
yield
48+
end
49+
elsif is_fragment_cached?
50+
FragmentCache.new(self, serializer, @options, @root).fetch
51+
else
52+
yield
53+
end
54+
end
55+
56+
def is_cached?
57+
@klass._cache && !@klass._cache_only && !@klass._cache_except
58+
end
59+
60+
def is_fragment_cached?
61+
@klass._cache_only && !@klass._cache_except || !@klass._cache_only && @klass._cache_except
62+
end
63+
64+
def cache_key
65+
(@klass._cache_key) ? "#{@klass._cache_key}/#{serializer.object.id}-#{serializer.object.updated_at}" : serializer.object.cache_key
66+
end
67+
3768
def meta
3869
serializer.meta if serializer.respond_to?(:meta)
3970
end
@@ -50,20 +81,6 @@ def include_meta(json)
5081
json[meta_key] = meta if meta && root
5182
json
5283
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
6784
end
6885
end
6986
end
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
# Get serializable hash from both
25+
cached_hash = @adapter.class.new(cached_serializer, @options).serializable_hash
26+
non_cached_hash = @adapter.class.new(non_cached_serializer, @options).serializable_hash
27+
28+
# Merge both results
29+
@adapter.fragment_cache(cached_hash, non_cached_hash)
30+
end
31+
32+
private
33+
34+
# Add associations to non-cached Serializer
35+
def fragment_associations(serializers, cached_attr, non_cached_attr)
36+
associations = @serializer.each_association
37+
cached_associations = associations.select {|k,v| cached_attr.include?(k)}
38+
non_cached_associations = associations.select {|k,v| !cached_attr.include?(k)}
39+
40+
# Add cached associations to cached Serializer
41+
cached_associations.each do |k,v|
42+
serializers[:cached].constantize.public_send(v[:type], k, v[:association_options])
43+
end
44+
45+
# Add not cached associations to non-cached Serializer
46+
non_cached_associations.each do |k,v|
47+
serializers[:non_cached].constantize.public_send(v[:type], k, v[:association_options])
48+
end
49+
end
50+
51+
def cached_attributes_and_association(klass, serializers)
52+
cached_attributes = (klass._cache_only) ? klass._cache_only : @serializer.attributes.keys.delete_if {|attr| klass._cache_except.include?(attr) }
53+
non_cached_attributes = @serializer.attributes.keys.delete_if {|attr| cached_attributes.include?(attr) }
54+
55+
fragment_associations(serializers, cached_attributes, non_cached_attributes)
56+
57+
cached_attributes.each do |attribute|
58+
options = @serializer.class._attributes_keys[attribute]
59+
options ||= {}
60+
# Add cached attributes to cached Serializer
61+
serializers[:cached].constantize.attribute(attribute, options)
62+
end
63+
64+
non_cached_attributes.each do |attribute|
65+
options = @serializer.class._attributes_keys[attribute]
66+
options ||= {}
67+
# Add non-cached attributes to non-cached Serializer
68+
serializers[:non_cached].constantize.attribute(attribute, options)
69+
end
70+
end
71+
72+
def fragment_serializer(name, klass)
73+
cached = "#{name.capitalize}CachedSerializer"
74+
non_cached = "#{name.capitalize}NonCachedSerializer"
75+
76+
Object.const_set cached, Class.new(ActiveModel::Serializer) unless Object.const_defined?(cached)
77+
Object.const_set non_cached, Class.new(ActiveModel::Serializer) unless Object.const_defined?(non_cached)
78+
79+
klass._cache_options ||= {}
80+
klass._cache_options[:key] = klass._cache_key if klass._cache_key
81+
cached.constantize.cache(klass._cache_options)
82+
83+
serializers = {cached: cached, non_cached: non_cached}
84+
cached_attributes_and_association(klass, serializers)
85+
serializers
86+
end
87+
end
88+
end
89+
end
90+
end

lib/active_model/serializer/adapter/json.rb

Lines changed: 7 additions & 1 deletion
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,7 +8,7 @@ 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
11+
@result = cache_check do
1012
@hash = serializer.attributes(options)
1113
serializer.each_association do |name, association, opts|
1214
if association.respond_to?(:each)
@@ -31,6 +33,10 @@ def serializable_hash(options = {})
3133
@result
3234
end
3335
end
36+
37+
def fragment_cache(cached_hash, non_cached_hash)
38+
Json::FragmentCache.new().fragment_cache(cached_hash, non_cached_hash)
39+
end
3440
end
3541
end
3642
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: 7 additions & 2 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,7 +25,7 @@ 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
28+
@hash = cache_check do
2729
@hash[@root] = attributes_for_serializer(serializer, @options)
2830
add_resource_links(@hash[@root], serializer)
2931
@hash
@@ -32,6 +34,10 @@ def serializable_hash(options = {})
3234
@hash
3335
end
3436

37+
def fragment_cache(cached_hash, non_cached_hash)
38+
JsonApi::FragmentCache.new().fragment_cache(@root, cached_hash, non_cached_hash)
39+
end
40+
3541
private
3642

3743
def add_links(resource, name, serializers)
@@ -91,7 +97,6 @@ def add_linked(resource_name, serializers, parent = nil)
9197
end
9298
end
9399

94-
95100
def attributes_for_serializer(serializer, options)
96101
if serializer.respond_to?(:each)
97102
result = []
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
if root
9+
hash = {}
10+
core_cached = cached_hash.first
11+
core_non_cached = non_cached_hash.first
12+
no_root_cache = cached_hash.delete_if {|key, value| key == core_cached[0] }
13+
no_root_non_cache = non_cached_hash.delete_if {|key, value| key == core_non_cached[0] }
14+
15+
cached_resource = (core_cached[1]) ? core_cached[1].merge(core_non_cached[1]) : core_non_cached[1]
16+
hash = (root) ? { root => cached_resource } : cached_resource
17+
18+
hash.merge no_root_non_cache.merge no_root_cache
19+
end
20+
end
21+
22+
end
23+
end
24+
end
25+
end
26+
end

test/action_controller/json_api_linked_test.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,14 @@ def test_render_resource_with_nested_has_many_include
110110
"roles"=>[{
111111
"id" => "1",
112112
"name" => "admin",
113+
"description" => nil,
113114
"links" => {
114115
"author" => "1"
115116
}
116117
}, {
117118
"id" => "2",
118119
"name" => "colab",
120+
"description" => nil,
119121
"links" => {
120122
"author" => "1"
121123
}

0 commit comments

Comments
 (0)