Skip to content

Commit 7df73c0

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 7df73c0

File tree

9 files changed

+150
-31
lines changed

9 files changed

+150
-31
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: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
require 'active_model/serializer/cache'
2+
13
module ActiveModel
24
class Serializer
35
class Adapter
46
extend ActiveSupport::Autoload
7+
include ActiveModel::Serializer::Cache
58
autoload :Json
69
autoload :Null
710
autoload :JsonApi
@@ -50,20 +53,6 @@ def include_meta(json)
5053
json[meta_key] = meta if meta && root
5154
json
5255
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
6756
end
6857
end
6958
end

lib/active_model/serializer/adapter/json_api.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ def add_linked(resource_name, serializers, parent = nil)
9191
end
9292
end
9393

94-
9594
def attributes_for_serializer(serializer, options)
9695
if serializer.respond_to?(:each)
9796
result = []

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
}

test/action_controller/serialization_test.rb

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,42 @@ def render_changed_object_with_cache_enabled
8181
render json: post
8282
end
8383

84+
def render_fragment_changed_object_with_only_cache_enabled
85+
author = Author.new(id: 1, name: 'Joao Moura.')
86+
role = Role.new({ id: 42, name: 'ZOMG A ROLE', description: 'DESCRIPTION HERE', author: author })
87+
88+
generate_cached_serializer(role)
89+
role.name = 'lol'
90+
role.description = 'HUEHUEBRBR'
91+
92+
render json: role
93+
end
94+
95+
def render_fragment_changed_object_with_except_cache_enabled
96+
author = Author.new(id: 1, name: 'Joao Moura.')
97+
bio = Bio.new({ id: 42, content: 'ZOMG A ROLE', rating: 5, author: author })
98+
99+
generate_cached_serializer(bio)
100+
bio.content = 'lol'
101+
bio.rating = 0
102+
103+
render json: bio
104+
end
105+
106+
def render_fragment_changed_object_with_relationship
107+
comment = Comment.new({ id: 1, body: 'ZOMG A COMMENT' })
108+
author = Author.new(id: 1, name: 'Joao Moura.')
109+
post = Post.new({ id: 1, title: 'New Post', blog:nil, body: 'Body', comments: [comment], author: author })
110+
post2 = Post.new({ id: 1, title: 'New Post2', blog:nil, body: 'Body2', comments: [comment], author: author })
111+
like = Like.new({ id: 1, post: post, time: 3.days.ago })
112+
113+
generate_cached_serializer(like)
114+
like.post = post2
115+
like.time = DateTime.now.to_s
116+
117+
render json: like
118+
end
119+
84120
private
85121
def generate_cached_serializer(obj)
86122
serializer_class = ActiveModel::Serializer.serializer_for(obj)
@@ -200,6 +236,45 @@ def test_render_with_cache_enable_and_expired
200236
assert_equal 'application/json', @response.content_type
201237
assert_equal expected.to_json, @response.body
202238
end
239+
240+
def test_render_with_fragment_only_cache_enable
241+
ActionController::Base.cache_store.clear
242+
get :render_fragment_changed_object_with_only_cache_enabled
243+
response = JSON.parse(@response.body)
244+
245+
assert_equal 'application/json', @response.content_type
246+
assert_equal 'ZOMG A ROLE', response["name"]
247+
assert_equal 'HUEHUEBRBR', response["description"]
248+
end
249+
250+
def test_render_with_fragment_except_cache_enable
251+
ActionController::Base.cache_store.clear
252+
get :render_fragment_changed_object_with_except_cache_enabled
253+
response = JSON.parse(@response.body)
254+
255+
assert_equal 'application/json', @response.content_type
256+
assert_equal 5, response["rating"]
257+
assert_equal 'lol', response["content"]
258+
end
259+
260+
def test_render_fragment_changed_object_with_relationship
261+
ActionController::Base.cache_store.clear
262+
get :render_fragment_changed_object_with_relationship
263+
response = JSON.parse(@response.body)
264+
265+
expected_return = {
266+
"post" => {
267+
"id"=>1,
268+
"title"=>"New Post",
269+
"body"=>"Body"
270+
},
271+
"id"=>1,
272+
"time"=>DateTime.now.to_s
273+
}
274+
275+
assert_equal 'application/json', @response.content_type
276+
assert_equal expected_return, response
277+
end
203278
end
204279
end
205-
end
280+
end

test/adapter/json_api/linked_test.rb

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ class Adapter
66
class JsonApi
77
class LinkedTest < Minitest::Test
88
def setup
9+
ActionController::Base.cache_store.clear
910
@author1 = Author.new(id: 1, name: 'Steve K.')
1011
@author2 = Author.new(id: 2, name: 'Tenderlove')
1112
@bio1 = Bio.new(id: 1, content: 'AMS Contributor')
@@ -32,7 +33,7 @@ def setup
3233
@bio2.author = @author2
3334
end
3435

35-
def test_include_multiple_posts_and_linked
36+
def test_include_multiple_posts_and_linked_serializer
3637
@serializer = ArraySerializer.new([@first_post, @second_post])
3738
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'author,author.bio,comments')
3839

@@ -44,8 +45,8 @@ def test_include_multiple_posts_and_linked
4445
@second_comment.post = @first_post
4546
@second_comment.author = nil
4647
assert_equal([
47-
{ title: "Hello!!", body: "Hello, world!!", id: "1", links: { comments: ['1', '2'], author: "1" } },
48-
{ title: "New Post", body: "Body", id: "2", links: { comments: [], :author => "2" } }
48+
{ :id=>"1", :title=>"Hello!!", :body=>"Hello, world!!", :links=>{:comments=>["1", "2"], :blog=>"999", :author=>"1"} },
49+
{ :id=>"2", :title=>"New Post", :body=>"Body", :links=>{:comments=>[], :blog=>"999", :author=>"2"} }
4950
], @adapter.serializable_hash[:posts])
5051

5152

@@ -69,7 +70,7 @@ def test_include_multiple_posts_and_linked
6970
id: "1",
7071
name: "Steve K.",
7172
links: {
72-
posts: ["1"],
73+
posts: ["1", "3"],
7374
roles: [],
7475
bio: "1"
7576
}
@@ -85,12 +86,14 @@ def test_include_multiple_posts_and_linked
8586
bios: [{
8687
id: "1",
8788
content: "AMS Contributor",
89+
rating: nil,
8890
links: {
8991
author: "1"
9092
}
9193
}, {
9294
id: "2",
9395
content: "Rails Contributor",
96+
rating: nil,
9497
links: {
9598
author: "2"
9699
}
@@ -99,9 +102,9 @@ def test_include_multiple_posts_and_linked
99102
assert_equal expected, @adapter.serializable_hash[:linked]
100103
end
101104

102-
def test_include_multiple_posts_and_linked
105+
def test_include_multiple_posts_and_linked_array_serializer
103106
@serializer = BioSerializer.new(@bio1)
104-
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'author,author.posts')
107+
@serializer.class.config.adapter = :json_api
105108

106109
@first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT')
107110
@second_comment = Comment.new(id: 2, body: 'ZOMG ANOTHER COMMENT')
@@ -111,6 +114,7 @@ def test_include_multiple_posts_and_linked
111114
@first_comment.author = nil
112115
@second_comment.post = @first_post
113116
@second_comment.author = nil
117+
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'author,author.posts')
114118

115119
expected = {
116120
authors: [{
@@ -142,7 +146,10 @@ def test_include_multiple_posts_and_linked
142146
}
143147
}]
144148
}
145-
assert_equal expected, @adapter.serializable_hash[:linked]
149+
hash = @adapter.serializable_hash
150+
151+
assert_equal :bios, hash.first[0]
152+
assert_equal expected, hash[:linked]
146153
end
147154

148155
def test_ignore_model_namespace_for_linked_resource_type

test/fixtures/poro.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class ProfilePreviewSerializer < ActiveModel::Serializer
5454
end
5555

5656
Post = Class.new(Model)
57+
Like = Class.new(Model)
5758
Comment = Class.new(Model)
5859
Author = Class.new(Model)
5960
Bio = Class.new(Model)
@@ -103,13 +104,22 @@ def self.root_name
103104
end
104105

105106
RoleSerializer = Class.new(ActiveModel::Serializer) do
106-
attributes :id, :name
107+
cache only: [:name]
108+
attributes :id, :name, :description
107109

108110
belongs_to :author
109111
end
110112

113+
LikeSerializer = Class.new(ActiveModel::Serializer) do
114+
cache only: [:post]
115+
attributes :id, :time
116+
117+
belongs_to :post
118+
end
119+
111120
BioSerializer = Class.new(ActiveModel::Serializer) do
112-
attributes :id, :content
121+
cache except: [:content]
122+
attributes :id, :content, :rating
113123

114124
belongs_to :author
115125
end

test/serializers/cache_test.rb

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,22 @@ module ActiveModel
33
class Serializer
44
class CacheTest < Minitest::Test
55
def setup
6-
@post = Post.new({ title: 'New Post', body: 'Body' })
7-
@comment = Comment.new({ id: 1, body: 'ZOMG A COMMENT' })
6+
@comment = Comment.new(id: 1, body: 'ZOMG A COMMENT')
7+
@post = Post.new(title: 'New Post', body: 'Body')
8+
@bio = Bio.new(id: 1, content: 'AMS Contributor')
89
@author = Author.new(name: 'Joao M. D. Moura')
910
@role = Role.new(name: 'Great Author')
1011
@author.posts = [@post]
1112
@author.roles = [@role]
12-
@author.bio = nil
13+
@role.author = @author
14+
@author.bio = @bio
1315
@post.comments = [@comment]
1416
@post.author = @author
1517
@comment.post = @post
1618
@comment.author = @author
1719

20+
@bio_serializer = BioSerializer.new(@bio)
21+
@role_serializer = RoleSerializer.new(@role)
1822
@post_serializer = PostSerializer.new(@post)
1923
@author_serializer = AuthorSerializer.new(@author)
2024
@comment_serializer = CommentSerializer.new(@comment)
@@ -33,13 +37,13 @@ def test_cache_key_definition
3337
end
3438

3539
def test_cache_key_interpolation_with_updated_at
36-
author = render_object_with_cache_without_cache_key(@author)
40+
author = render_object_with_cache(@author)
3741
assert_equal(nil, ActionController::Base.cache_store.fetch(@author.cache_key))
3842
assert_equal(author, ActionController::Base.cache_store.fetch("#{@author_serializer.class._cache_key}/#{@author_serializer.object.id}-#{@author_serializer.object.updated_at}").to_json)
3943
end
4044

4145
def test_default_cache_key_fallback
42-
comment = render_object_with_cache_without_cache_key(@comment)
46+
comment = render_object_with_cache(@comment)
4347
assert_equal(comment, ActionController::Base.cache_store.fetch(@comment.cache_key).to_json)
4448
end
4549

@@ -49,8 +53,13 @@ def test_cache_options_definition
4953
assert_equal({expires_in: 1.day}, @comment_serializer.class._cache_options)
5054
end
5155

56+
def test_fragment_cache_definition
57+
assert_equal([:name], @role_serializer.class._cache_only)
58+
assert_equal([:content], @bio_serializer.class._cache_except)
59+
end
60+
5261
private
53-
def render_object_with_cache_without_cache_key(obj)
62+
def render_object_with_cache(obj)
5463
serializer_class = ActiveModel::Serializer.serializer_for(obj)
5564
serializer = serializer_class.new(obj)
5665
adapter = ActiveModel::Serializer.adapter.new(serializer)

0 commit comments

Comments
 (0)