Refactor caching support (#52)

- Very explicit where caching is stored
- No `Rails.cache` automagic with possible colliding keys
- Rails 5.2+ cache versioning support
- Implicit namespace support (by cache instance, e.g.
  ActiveSupport::Cache::MemoryStore.new(namespace: 'jast_jsonapi')
- All cache instance options are supported
This commit is contained in:
Markus 2020-02-25 17:49:02 +01:00 committed by GitHub
parent 3faca2d1e0
commit 843d943e07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 93 additions and 24 deletions

View File

@ -362,17 +362,31 @@ was introduced to be able to have precise control this behavior
- `false` will always treat input resource as *single object*
### Caching
Requires a `cache_key` method be defined on model:
To enable caching, use `cache_options store: <cache_store>`:
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
set_type :movie # optional
cache_options enabled: true, cache_length: 12.hours
attributes :name, :year
# use rails cache with a separate namespace and fixed expiry
cache_options store: Rails.cache, namespace: 'fast-jsonapi', expires_in: 1.hour
end
```
`store` is required can be anything that implements a
`#fetch(record, **options, &block)` method:
- `record` is the record that is currently serialized
- `options` is everything that was passed to `cache_options` except `store`, so it can be everyhing the cache store supports
- `&block` should be executed to fetch new data if cache is empty
So for the example above, FastJsonapi will call the cache instance like this:
```ruby
Rails.cache.fetch(record, namespace: 'fast-jsonapi, expires_in: 1.hour) { ... }
```
### Params
In some cases, attribute values might require more information than what is
@ -592,7 +606,7 @@ Option | Purpose | Example
set_type | Type name of Object | `set_type :movie`
key | Key of Object | `belongs_to :owner, key: :user`
set_id | ID of Object | `set_id :owner_id` or `set_id { \|record, params\| params[:admin] ? record.id : "#{record.name.downcase}-#{record.id}" }`
cache_options | Hash to enable caching and set cache length | `cache_options enabled: true, cache_length: 12.hours, race_condition_ttl: 10.seconds`
cache_options | Hash with store to enable caching and optional further cache options | `cache_options store: ActiveSupport::Cache::MemoryStore.new, expires_in: 5.minutes`
id_method_name | Set custom method name to get ID of an object (If block is provided for the relationship, `id_method_name` is invoked on the return value of the block instead of the resource object) | `has_many :locations, id_method_name: :place_ids`
object_method_name | Set custom method name to get related objects | `has_many :locations, object_method_name: :places`
record_type | Set custom Object Type for a relationship | `belongs_to :owner, record_type: :user`

View File

@ -129,10 +129,9 @@ module FastJsonapi
subclass.cachable_relationships_to_serialize = cachable_relationships_to_serialize.dup if cachable_relationships_to_serialize.present?
subclass.uncachable_relationships_to_serialize = uncachable_relationships_to_serialize.dup if uncachable_relationships_to_serialize.present?
subclass.transform_method = transform_method
subclass.cache_length = cache_length
subclass.race_condition_ttl = race_condition_ttl
subclass.data_links = data_links.dup if data_links.present?
subclass.cached = cached
subclass.cache_store_instance = cache_store_instance
subclass.cache_store_options = cache_store_options
subclass.set_type(subclass.reflected_record_type) if subclass.reflected_record_type
subclass.meta_to_serialize = meta_to_serialize
subclass.record_id = record_id
@ -181,9 +180,32 @@ module FastJsonapi
end
def cache_options(cache_options)
self.cached = cache_options[:enabled] || false
self.cache_length = cache_options[:cache_length] || 5.minutes
self.race_condition_ttl = cache_options[:race_condition_ttl] || 5.seconds
# FIXME: remove this if block once deprecated cache_options are not supported anymore
if !cache_options.key?(:store)
# fall back to old, deprecated behaviour because no store was passed.
# we assume the user explicitly wants new behaviour if he passed a
# store because this is the new syntax.
deprecated_cache_options(cache_options)
return
end
self.cache_store_instance = cache_options[:store]
self.cache_store_options = cache_options.except(:store)
end
# FIXME: remove this method once deprecated cache_options are not supported anymore
def deprecated_cache_options(cache_options)
warn('DEPRECATION WARNING: `store:` is a required cache option, we will default to `Rails.cache` for now. See https://github.com/fast-jsonapi/fast_jsonapi#caching for more information.')
%i[enabled cache_length].select { |key| cache_options.key?(key) }.each do |key|
warn("DEPRECATION WARNING: `#{key}` is a deprecated cache option and will have no effect soon. See https://github.com/fast-jsonapi/fast_jsonapi#caching for more information.")
end
self.cache_store_instance = cache_options[:enabled] ? Rails.cache : nil
self.cache_store_options = {
expires_in: cache_options[:cache_length] || 5.minutes,
race_condition_ttl: cache_options[:race_condition_ttl] || 5.seconds
}
end
def attributes(*attributes_list, &block)

View File

@ -17,9 +17,8 @@ module FastJsonapi
:transform_method,
:record_type,
:record_id,
:cache_length,
:race_condition_ttl,
:cached,
:cache_store_instance,
:cache_store_options,
:data_links,
:meta_to_serialize
end
@ -66,8 +65,8 @@ module FastJsonapi
end
def record_hash(record, fieldset, includes_list, params = {})
if cached
record_hash = Rails.cache.fetch(record.cache_key, expires_in: cache_length, race_condition_ttl: race_condition_ttl) do
if cache_store_instance
record_hash = cache_store_instance.fetch(record, **cache_store_options) do
temp_hash = id_hash(id_from_record(record, params), record_type, true)
temp_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present?
temp_hash[:relationships] = {}

View File

@ -4,12 +4,6 @@ describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
context 'when caching has_many' do
before(:each) do
rails = OpenStruct.new
rails.cache = ActiveSupport::Cache::MemoryStore.new
stub_const('Rails', rails)
end
it 'returns correct hash when serializable_hash is called' do
options = {}
options[:meta] = { total: 2 }
@ -70,4 +64,44 @@ describe FastJsonapi::ObjectSerializer do
expect(serializable_hash[:data][:relationships][:actors][:data].length).to eq previous_actors.length
end
end
# FIXME: remove this if block once deprecated cache_options are not supported anymore
context 'when using deprecated cache options' do
let(:deprecated_caching_movie_serializer_class) do
rails = OpenStruct.new
rails.cache = ActiveSupport::Cache::MemoryStore.new
stub_const('Rails', rails)
Class.new do
def self.name
'DeprecatedCachingMovieSerializer'
end
include FastJsonapi::ObjectSerializer
set_type :movie
attributes :name, :release_year
has_many :actors
belongs_to :owner, record_type: :user
belongs_to :movie_type
cache_options enabled: true
end
end
it 'uses cached values for the record' do
previous_name = movie.name
previous_actors = movie.actors
deprecated_caching_movie_serializer_class.new(movie).serializable_hash
movie.name = 'should not match'
allow(movie).to receive(:actor_ids).and_return([99])
expect(previous_name).not_to eq(movie.name)
expect(previous_actors).not_to eq(movie.actors)
serializable_hash = deprecated_caching_movie_serializer_class.new(movie).serializable_hash
expect(serializable_hash[:data][:attributes][:name]).to eq(previous_name)
expect(serializable_hash[:data][:relationships][:actors][:data].length).to eq movie.actors.length
end
end
end

View File

@ -222,7 +222,7 @@ RSpec.shared_context 'movie class' do
belongs_to :owner, record_type: :user
belongs_to :movie_type
cache_options enabled: true
cache_options store: ActiveSupport::Cache::MemoryStore.new, expires_in: 5.minutes
end
class CachingMovieWithHasManySerializer
@ -233,7 +233,7 @@ RSpec.shared_context 'movie class' do
belongs_to :owner, record_type: :user
belongs_to :movie_type
cache_options enabled: true
cache_options store: ActiveSupport::Cache::MemoryStore.new, namespace: 'fast-jsonapi'
end
class ActorSerializer