Skip to content

Commit 3d9ab37

Browse files
committed
Merge pull request #1781 from bf4/serializer_cleanup_6
Fix up caching, especially fragment_cache
2 parents d191342 + b599360 commit 3d9ab37

File tree

15 files changed

+136
-160
lines changed

15 files changed

+136
-160
lines changed

lib/active_model/serializer.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def serializable_hash(adapter_options = nil, options = {}, adapter_instance = se
167167
adapter_options ||= {}
168168
options[:include_directive] ||= ActiveModel::Serializer.include_directive_from_options(adapter_options)
169169
cached_attributes = adapter_options[:cached_attributes] ||= {}
170-
resource = cached_attributes(options[:fields], cached_attributes, adapter_instance)
170+
resource = fetch_attributes(options[:fields], cached_attributes, adapter_instance)
171171
relationships = resource_relationships(adapter_options, options, adapter_instance)
172172
resource.merge(relationships)
173173
end
@@ -194,8 +194,6 @@ def json_key
194194
def read_attribute_for_serialization(attr)
195195
if respond_to?(attr)
196196
send(attr)
197-
elsif self.class._fragmented
198-
self.class._fragmented.read_attribute_for_serialization(attr)
199197
else
200198
object.read_attribute_for_serialization(attr)
201199
end

lib/active_model/serializer/adapter/base.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ class << self
77
deprecate :inherited, 'ActiveModelSerializers::Adapter::Base.'
88
end
99

10+
# :nocov:
1011
def initialize(serializer, options = {})
1112
super(ActiveModelSerializers::Adapter::Base.new(serializer, options))
1213
end
14+
# :nocov:
1315
end
1416
end
1517
end

lib/active_model/serializer/belongs_to_reflection.rb

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ module ActiveModel
22
class Serializer
33
# @api private
44
class BelongsToReflection < SingularReflection
5-
def macro
6-
:belongs_to
7-
end
85
end
96
end
107
end

lib/active_model/serializer/caching.rb

Lines changed: 51 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
# TODO(BF): refactor file to be smaller
2-
# rubocop:disable Metrics/ModuleLength
31
module ActiveModel
42
class Serializer
53
UndefinedCacheKey = Class.new(StandardError)
@@ -9,10 +7,9 @@ module Caching
97
included do
108
with_options instance_writer: false, instance_reader: false do |serializer|
119
serializer.class_attribute :_cache # @api private : the cache store
12-
serializer.class_attribute :_fragmented # @api private : @see ::fragmented
1310
serializer.class_attribute :_cache_key # @api private : when present, is first item in cache_key. Ignored if the serializable object defines #cache_key.
14-
serializer.class_attribute :_cache_only # @api private : when fragment caching, whitelists cached_attributes. Cannot combine with except
15-
serializer.class_attribute :_cache_except # @api private : when fragment caching, blacklists cached_attributes. Cannot combine with only
11+
serializer.class_attribute :_cache_only # @api private : when fragment caching, whitelists fetch_attributes. Cannot combine with except
12+
serializer.class_attribute :_cache_except # @api private : when fragment caching, blacklists fetch_attributes. Cannot combine with only
1613
serializer.class_attribute :_cache_options # @api private : used by CachedSerializer, passed to _cache.fetch
1714
# _cache_options include:
1815
# expires_in
@@ -21,7 +18,7 @@ module Caching
2118
# race_condition_ttl
2219
# Passed to ::_cache as
2320
# serializer.cache_store.fetch(cache_key, @klass._cache_options)
24-
# Passed as second argument to serializer.cache_store.fetch(cache_key, self.class._cache_options)
21+
# Passed as second argument to serializer.cache_store.fetch(cache_key, serializer_class._cache_options)
2522
serializer.class_attribute :_cache_digest_file_path # @api private : Derived at inheritance
2623
end
2724
end
@@ -71,19 +68,15 @@ def _skip_digest?
7168
_cache_options && _cache_options[:skip_digest]
7269
end
7370

74-
def cached_attributes
75-
_cache_only ? _cache_only : _attributes - _cache_except
76-
end
77-
78-
def non_cached_attributes
79-
_attributes - cached_attributes
80-
end
81-
82-
# @api private
83-
# Used by FragmentCache on the CachedSerializer
84-
# to call attribute methods on the fragmented cached serializer.
85-
def fragmented(serializer)
86-
self._fragmented = serializer
71+
def fragmented_attributes
72+
cached = _cache_only ? _cache_only : _attributes - _cache_except
73+
cached = cached.map! { |field| _attributes_keys.fetch(field, field) }
74+
non_cached = _attributes - cached
75+
non_cached = non_cached.map! { |field| _attributes_keys.fetch(field, field) }
76+
{
77+
cached: cached,
78+
non_cached: non_cached
79+
}
8780
end
8881

8982
# Enables a serializer to be automatically cached
@@ -208,118 +201,65 @@ def object_cache_key(serializer, adapter_instance)
208201
end
209202
end
210203

211-
def cached_attributes(fields, cached_attributes, adapter_instance)
212-
if self.class.cache_enabled?
204+
### INSTANCE METHODS
205+
def fetch_attributes(fields, cached_attributes, adapter_instance)
206+
if serializer_class.cache_enabled?
213207
key = cache_key(adapter_instance)
214208
cached_attributes.fetch(key) do
215-
cache_check(adapter_instance) do
216-
attributes(fields)
209+
serializer_class.cache_store.fetch(key, serializer_class._cache_options) do
210+
attributes(fields, true)
217211
end
218212
end
213+
elsif serializer_class.fragment_cache_enabled?
214+
fetch_attributes_fragment(adapter_instance)
219215
else
220-
cache_check(adapter_instance) do
221-
attributes(fields)
222-
end
216+
attributes(fields, true)
223217
end
224218
end
225219

226-
def cache_check(adapter_instance)
227-
if self.class.cache_enabled?
228-
self.class.cache_store.fetch(cache_key(adapter_instance), self.class._cache_options) do
220+
def fetch(adapter_instance, cache_options = serializer_class._cache_options)
221+
if serializer_class.cache_store
222+
serializer_class.cache_store.fetch(cache_key(adapter_instance), cache_options) do
229223
yield
230224
end
231-
elsif self.class.fragment_cache_enabled?
232-
fetch_fragment_cache(adapter_instance)
233225
else
234226
yield
235227
end
236228
end
237229

238-
# 1. Create a CachedSerializer and NonCachedSerializer from the serializer class
239-
# 2. Serialize the above two with the given adapter
240-
# 3. Pass their serializations to the adapter +::fragment_cache+
241-
#
242-
# It will split the serializer into two, one that will be cached and one that will not
243-
#
244-
# Given a resource name
245-
# 1. Dynamically creates a CachedSerializer and NonCachedSerializer
246-
# for a given class 'name'
247-
# 2. Call
248-
# CachedSerializer.cache(serializer._cache_options)
249-
# CachedSerializer.fragmented(serializer)
250-
# NonCachedSerializer.cache(serializer._cache_options)
251-
# 3. Build a hash keyed to the +cached+ and +non_cached+ serializers
252-
# 4. Call +cached_attributes+ on the serializer class and the above hash
253-
# 5. Return the hash
254-
#
255-
# @example
256-
# When +name+ is <tt>User::Admin</tt>
257-
# creates the Serializer classes (if they don't exist).
258-
# CachedUser_AdminSerializer
259-
# NonCachedUser_AdminSerializer
260-
#
261-
# Given a hash of its cached and non-cached serializers
262-
# 1. Determine cached attributes from serializer class options
263-
# 2. Add cached attributes to cached Serializer
264-
# 3. Add non-cached attributes to non-cached Serializer
265-
def fetch_fragment_cache(adapter_instance)
266-
serializer_class_name = self.class.name.gsub('::'.freeze, '_'.freeze)
267-
self.class._cache_options ||= {}
268-
self.class._cache_options[:key] = self.class._cache_key if self.class._cache_key
269-
270-
cached_serializer = _get_or_create_fragment_cached_serializer(serializer_class_name)
271-
cached_hash = ActiveModelSerializers::SerializableResource.new(
272-
object,
273-
serializer: cached_serializer,
274-
adapter: adapter_instance.class
275-
).serializable_hash
276-
277-
non_cached_serializer = _get_or_create_fragment_non_cached_serializer(serializer_class_name)
278-
non_cached_hash = ActiveModelSerializers::SerializableResource.new(
279-
object,
280-
serializer: non_cached_serializer,
281-
adapter: adapter_instance.class
282-
).serializable_hash
230+
# 1. Determine cached fields from serializer class options
231+
# 2. Get non_cached_fields and fetch cache_fields
232+
# 3. Merge the two hashes using adapter_instance#fragment_cache
233+
def fetch_attributes_fragment(adapter_instance)
234+
serializer_class._cache_options ||= {}
235+
serializer_class._cache_options[:key] = serializer_class._cache_key if serializer_class._cache_key
236+
fields = serializer_class.fragmented_attributes
237+
238+
non_cached_fields = fields[:non_cached].dup
239+
non_cached_hash = attributes(non_cached_fields, true)
240+
include_directive = JSONAPI::IncludeDirective.new(non_cached_fields - non_cached_hash.keys)
241+
non_cached_hash.merge! resource_relationships({}, { include_directive: include_directive }, adapter_instance)
242+
243+
cached_fields = fields[:cached].dup
244+
key = cache_key(adapter_instance)
245+
cached_hash =
246+
serializer_class.cache_store.fetch(key, serializer_class._cache_options) do
247+
hash = attributes(cached_fields, true)
248+
include_directive = JSONAPI::IncludeDirective.new(cached_fields - hash.keys)
249+
hash.merge! resource_relationships({}, { include_directive: include_directive }, adapter_instance)
250+
end
283251

284252
# Merge both results
285253
adapter_instance.fragment_cache(cached_hash, non_cached_hash)
286254
end
287255

288-
def _get_or_create_fragment_cached_serializer(serializer_class_name)
289-
cached_serializer = _get_or_create_fragment_serializer "Cached#{serializer_class_name}"
290-
cached_serializer.cache(self.class._cache_options)
291-
cached_serializer.type(self.class._type)
292-
cached_serializer.fragmented(self)
293-
self.class.cached_attributes.each do |attribute|
294-
options = self.class._attributes_keys[attribute] || {}
295-
cached_serializer.attribute(attribute, options)
296-
end
297-
cached_serializer
298-
end
299-
300-
def _get_or_create_fragment_non_cached_serializer(serializer_class_name)
301-
non_cached_serializer = _get_or_create_fragment_serializer "NonCached#{serializer_class_name}"
302-
non_cached_serializer.type(self.class._type)
303-
non_cached_serializer.fragmented(self)
304-
self.class.non_cached_attributes.each do |attribute|
305-
options = self.class._attributes_keys[attribute] || {}
306-
non_cached_serializer.attribute(attribute, options)
307-
end
308-
non_cached_serializer
309-
end
310-
311-
def _get_or_create_fragment_serializer(name)
312-
return Object.const_get(name) if Object.const_defined?(name)
313-
Object.const_set(name, Class.new(ActiveModel::Serializer))
314-
end
315-
316256
def cache_key(adapter_instance)
317257
return @cache_key if defined?(@cache_key)
318258

319259
parts = []
320260
parts << object_cache_key
321261
parts << adapter_instance.cache_key
322-
parts << self.class._cache_digest unless self.class._skip_digest?
262+
parts << serializer_class._cache_digest unless serializer_class._skip_digest?
323263
@cache_key = parts.join('/')
324264
end
325265

@@ -328,15 +268,18 @@ def cache_key(adapter_instance)
328268
def object_cache_key
329269
if object.respond_to?(:cache_key)
330270
object.cache_key
331-
elsif (serializer_cache_key = (self.class._cache_key || self.class._cache_options[:key]))
271+
elsif (serializer_cache_key = (serializer_class._cache_key || serializer_class._cache_options[:key]))
332272
object_time_safe = object.updated_at
333273
object_time_safe = object_time_safe.strftime('%Y%m%d%H%M%S%9N') if object_time_safe.respond_to?(:strftime)
334274
"#{serializer_cache_key}/#{object.id}-#{object_time_safe}"
335275
else
336-
fail UndefinedCacheKey, "#{object.class} must define #cache_key, or the 'key:' option must be passed into '#{self.class}.cache'"
276+
fail UndefinedCacheKey, "#{object.class} must define #cache_key, or the 'key:' option must be passed into '#{serializer_class}.cache'"
337277
end
338278
end
279+
280+
def serializer_class
281+
@serializer_class ||= self.class
282+
end
339283
end
340284
end
341285
end
342-
# rubocop:enable Metrics/ModuleLength

lib/active_model/serializer/has_many_reflection.rb

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ module ActiveModel
22
class Serializer
33
# @api private
44
class HasManyReflection < CollectionReflection
5-
def macro
6-
:has_many
7-
end
85
end
96
end
107
end

lib/active_model/serializer/has_one_reflection.rb

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ module ActiveModel
22
class Serializer
33
# @api private
44
class HasOneReflection < SingularReflection
5-
def macro
6-
:has_one
7-
end
85
end
96
end
107
end

lib/active_model_serializers/adapter.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ module Adapter
55
private_constant :ADAPTER_MAP if defined?(private_constant)
66

77
class << self # All methods are class functions
8+
# :nocov:
89
def new(*args)
910
fail ArgumentError, 'Adapters inherit from Adapter::Base.' \
1011
"Adapter.new called with args: '#{args.inspect}', from" \
1112
"'caller[0]'."
1213
end
14+
# :nocov:
1315

1416
def configured_adapter
1517
lookup(ActiveModelSerializers.config.adapter)

lib/active_model_serializers/adapter/json_api.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ def attributes_for(serializer, fields)
294294

295295
# {http://jsonapi.org/format/#document-resource-objects Document Resource Objects}
296296
def resource_object_for(serializer)
297-
resource_object = serializer.cache_check(self) do
297+
resource_object = serializer.fetch(self) do
298298
resource_object = ResourceIdentifier.new(serializer, instance_options).as_json
299299

300300
requested_fields = fieldset && fieldset.fields_for(resource_object[:type])

lib/active_model_serializers/deserialization.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ def jsonapi_parse(*args)
66
Adapter::JsonApi::Deserialization.parse(*args)
77
end
88

9+
# :nocov:
910
def jsonapi_parse!(*args)
1011
Adapter::JsonApi::Deserialization.parse!(*args)
1112
end
13+
# :nocov:
1214
end
1315
end

lib/active_model_serializers/model.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,14 @@ def read_attribute_for_serialization(key)
3838
end
3939

4040
# The following methods are needed to be minimally implemented for ActiveModel::Errors
41+
# :nocov:
4142
def self.human_attribute_name(attr, _options = {})
4243
attr
4344
end
4445

4546
def self.lookup_ancestors
4647
[self]
4748
end
49+
# :nocov:
4850
end
4951
end

lib/active_model_serializers/railtie.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ class Railtie < Rails::Railtie
3232
end
3333
end
3434

35+
# :nocov:
3536
generators do |app|
3637
Rails::Generators.configure!(app.config.generators)
3738
Rails::Generators.hidden_namespaces.uniq!
3839
require 'generators/rails/resource_override'
3940
end
41+
# :nocov:
4042

4143
if Rails.env.test?
4244
ActionController::TestCase.send(:include, ActiveModelSerializers::Test::Schema)

test/action_controller/explicit_serializer_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def test_render_using_explicit_each_serializer
123123
id: 42,
124124
lat: '-23.550520',
125125
lng: '-46.633309',
126-
place: 'Nowhere' # is a virtual attribute on LocationSerializer
126+
address: 'Nowhere' # is a virtual attribute on LocationSerializer
127127
}
128128
]
129129
}

test/adapter/json_api/resource_identifier_test.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ def id
1414
end
1515
end
1616

17-
class FragmentedSerializer < ActiveModel::Serializer; end
17+
class FragmentedSerializer < ActiveModel::Serializer
18+
cache only: :id
19+
20+
def id
21+
'special_id'
22+
end
23+
end
1824

1925
setup do
2026
@model = Author.new(id: 1, name: 'Steve K.')
@@ -42,7 +48,6 @@ def test_id_defined_on_serializer
4248
end
4349

4450
def test_id_defined_on_fragmented
45-
FragmentedSerializer.fragmented(WithDefinedIdSerializer.new(@model))
4651
test_id(FragmentedSerializer, 'special_id')
4752
end
4853

0 commit comments

Comments
 (0)