Skip to content

Cache Support at AMS 0.10.0 #693

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 4, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ coverage
doc/
lib/bundler/man
pkg
Vagrantfile
.vagrant
rdoc
spec/reports
test/tmp
Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ serializers:

```ruby
class PostSerializer < ActiveModel::Serializer
cache key: 'posts', expires_in: 3.hours
attributes :title, :body

has_many :comments
Expand Down Expand Up @@ -246,6 +247,37 @@ You may also use the `:serializer` option to specify a custom serializer class,
The `url` declaration describes which named routes to use while generating URLs
for your JSON. Not every adapter will require URLs.

## Caching

To cache a serializer, call ```cache``` and pass its options.
The options are the same options of ```ActiveSupport::Cache::Store```, plus
a ```key``` option that will be the prefix of the object cache
on a pattern ```"#{key}/#{object.id}-#{object.updated_at}"```.

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

```ruby
cache(options = nil) # options: ```{key, expires_in, compress, force, race_condition_ttl}```
```

Take the example bellow:

```ruby
class PostSerializer < ActiveModel::Serializer
cache key: 'post', expires_in: 3.hours
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joaomdmoura could we make key optional? Then we could use object.cache_key by default. Makes sense?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kurko I'm not sure if I have already done it 😕 But I'll check it.

attributes :title, :body

has_many :comments

url :post
end
```

On this example every ```Post``` object will be cached with
the key ```"post/#{post.id}-#{post.updated_at}"```. You can use this key to expire it as you want,
but in this case it will be automatically expired after 3 hours.

## Getting Help

If you find a bug, please report an [Issue](https://github.com/rails-api/active_model_serializers/issues/new).
Expand Down
12 changes: 11 additions & 1 deletion lib/active_model/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ class << self
attr_accessor :_attributes
attr_accessor :_associations
attr_accessor :_urls
attr_accessor :_cache
attr_accessor :_cache_key
attr_accessor :_cache_options
end

def self.inherited(base)
Expand All @@ -36,7 +39,14 @@ def self.attribute(attr, options = {})
end unless method_defined?(key)
end

# Defines an association in the object that should be rendered.
# 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_options = (options.empty?) ? nil : options
end

# Defines an association in the object should be rendered.
#
# The serializer object should implement the association name
# as a method which should return an array when invoked. If a method
Expand Down
14 changes: 14 additions & 0 deletions lib/active_model/serializer/adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ def include_meta(json)
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
22 changes: 12 additions & 10 deletions lib/active_model/serializer/adapter/json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@ def serializable_hash(options = {})
if serializer.respond_to?(:each)
@result = serializer.map{|s| self.class.new(s).serializable_hash }
else
@result = serializer.attributes(options)

serializer.each_association do |name, association, opts|
if association.respond_to?(:each)
array_serializer = association
@result[name] = array_serializer.map { |item| item.attributes(opts) }
else
if association
@result[name] = association.attributes(options)
@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
@result[name] = nil
if association
@hash[name] = association.attributes(options)
else
@hash[name] = nil
end
end
end
@hash
end
end

Expand Down
8 changes: 5 additions & 3 deletions lib/active_model/serializer/adapter/json_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ def serializable_hash(options = {})
self.class.new(s, @options.merge(top: @top, fieldset: @fieldset)).serializable_hash[@root]
end
else
@hash[@root] = attributes_for_serializer(serializer, @options)
add_resource_links(@hash[@root], serializer)
@hash = cached_object do
@hash[@root] = attributes_for_serializer(serializer, @options)
add_resource_links(@hash[@root], serializer)
@hash
end
end

@hash
end

Expand Down
8 changes: 4 additions & 4 deletions lib/active_model_serializers.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require "active_model"
require "active_model/serializer/version"
require "active_model/serializer"
require "active_model/serializer/fieldset"
require 'active_model'
require 'active_model/serializer/version'
require 'active_model/serializer'
require 'active_model/serializer/fieldset'

begin
require 'action_controller'
Expand Down
1 change: 1 addition & 0 deletions test/action_controller/json_api_linked_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Serialization
class JsonApiLinkedTest < ActionController::TestCase
class MyController < ActionController::Base
def setup_post
ActionController::Base.cache_store.clear
@role1 = Role.new(id: 1, name: 'admin')
@role2 = Role.new(id: 2, name: 'colab')
@author = Author.new(id: 1, name: 'Steve K.')
Expand Down
96 changes: 95 additions & 1 deletion test/action_controller/serialization_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,48 @@ def render_array_using_implicit_serializer_and_meta
Profile.new({ name: 'Name 1', description: 'Description 1', comments: 'Comments 1' })
]
render json: array, meta: { total: 10 }
ensure
ensure
ActiveModel::Serializer.config.adapter = old_adapter
end

def render_object_with_cache_enabled
comment = Comment.new({ id: 1, body: 'ZOMG A COMMENT' })
author = Author.new(id: 1, name: 'Joao Moura.')
post = Post.new({ id: 1, title: 'New Post', blog:nil, body: 'Body', comments: [comment], author: author })

generate_cached_serializer(post)

post.title = 'ZOMG a New Post'
render json: post
end

def render_object_expired_with_cache_enabled
comment = Comment.new({ id: 1, body: 'ZOMG A COMMENT' })
author = Author.new(id: 1, name: 'Joao Moura.')
post = Post.new({ id: 1, title: 'New Post', blog:nil, body: 'Body', comments: [comment], author: author })

generate_cached_serializer(post)

post.title = 'ZOMG a New Post'
sleep 0.05
render json: post
end

def render_changed_object_with_cache_enabled
comment = Comment.new({ id: 1, body: 'ZOMG A COMMENT' })
author = Author.new(id: 1, name: 'Joao Moura.')
post = Post.new({ id: 1, title: 'ZOMG a New Post', blog:nil, body: 'Body', comments: [comment], author: author })

render json: post
end

private
def generate_cached_serializer(obj)
serializer_class = ActiveModel::Serializer.serializer_for(obj)
serializer = serializer_class.new(obj)
adapter = ActiveModel::Serializer.adapter.new(serializer)
adapter.to_json
end
end

tests MyController
Expand Down Expand Up @@ -106,6 +145,61 @@ def test_render_array_using_implicit_serializer_and_meta
assert_equal 'application/json', @response.content_type
assert_equal '{"profiles":[{"name":"Name 1","description":"Description 1"}],"meta":{"total":10}}', @response.body
end

def test_render_with_cache_enable
ActionController::Base.cache_store.clear
get :render_object_with_cache_enabled

expected = {
id: 1,
title: 'New Post',
body: 'Body',
comments: [
{
id: 1,
body: 'ZOMG A COMMENT' }
],
blog: nil,
author: {
id: 1,
name: 'Joao Moura.'
}
}

assert_equal 'application/json', @response.content_type
assert_equal expected.to_json, @response.body

get :render_changed_object_with_cache_enabled
assert_equal expected.to_json, @response.body

ActionController::Base.cache_store.clear
get :render_changed_object_with_cache_enabled
assert_not_equal expected.to_json, @response.body
end

def test_render_with_cache_enable_and_expired
ActionController::Base.cache_store.clear
get :render_object_expired_with_cache_enabled

expected = {
id: 1,
title: 'ZOMG a New Post',
body: 'Body',
comments: [
{
id: 1,
body: 'ZOMG A COMMENT' }
],
blog: nil,
author: {
id: 1,
name: 'Joao Moura.'
}
}

assert_equal 'application/json', @response.content_type
assert_equal expected.to_json, @response.body
end
end
end
end
1 change: 1 addition & 0 deletions test/adapter/json/belongs_to_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def setup

@serializer = CommentSerializer.new(@comment)
@adapter = ActiveModel::Serializer::Adapter::Json.new(@serializer)
ActionController::Base.cache_store.clear
end

def test_includes_post
Expand Down
1 change: 1 addition & 0 deletions test/adapter/json/collection_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def setup

@serializer = ArraySerializer.new([@first_post, @second_post])
@adapter = ActiveModel::Serializer::Adapter::Json.new(@serializer)
ActionController::Base.cache_store.clear
end

def test_include_multiple_posts
Expand Down
1 change: 1 addition & 0 deletions test/adapter/json_api/belongs_to_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def setup

@serializer = CommentSerializer.new(@comment)
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer)
ActionController::Base.cache_store.clear
end

def test_includes_post_id
Expand Down
1 change: 1 addition & 0 deletions test/adapter/json_api/collection_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def setup

@serializer = ArraySerializer.new([@first_post, @second_post])
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer)
ActionController::Base.cache_store.clear
end

def test_include_multiple_posts
Expand Down
1 change: 1 addition & 0 deletions test/adapter/json_api/has_many_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class Adapter
class JsonApi
class HasManyTest < Minitest::Test
def setup
ActionController::Base.cache_store.clear
@author = Author.new(id: 1, name: 'Steve K.')
@author.posts = []
@author.bio = nil
Expand Down
13 changes: 12 additions & 1 deletion test/fixtures/poro.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ def initialize(hash={})
@attributes = hash
end

def cache_key
"#{self.class.name.downcase}/#{self.id}-#{self.updated_at}"
end

def updated_at
@attributes[:updated_at] ||= DateTime.now.to_time.to_i
end

def read_attribute_for_serialization(name)
if name == :id || name == 'id'
id
Expand Down Expand Up @@ -55,7 +63,8 @@ module Spam; end
Spam::UnrelatedLink = Class.new(Model)

PostSerializer = Class.new(ActiveModel::Serializer) do
attributes :title, :body, :id
cache key:'post', expires_in: 0.05
attributes :id, :title, :body

has_many :comments
belongs_to :blog
Expand All @@ -77,13 +86,15 @@ def self.root_name
end

CommentSerializer = Class.new(ActiveModel::Serializer) do
cache expires_in: 1.day
attributes :id, :body

belongs_to :post
belongs_to :author
end

AuthorSerializer = Class.new(ActiveModel::Serializer) do
cache key:'writer'
attributes :id, :name

has_many :posts, embed: :ids
Expand Down
Loading