Skip to content

Commit 0f59d64

Browse files
authored
Merge pull request #2026 from bf4/refactor_association
Refactor Association to make it eval reflection JIT
2 parents 3fb72d9 + 8761904 commit 0f59d64

File tree

14 files changed

+298
-193
lines changed

14 files changed

+298
-193
lines changed

lib/active_model/serializer.rb

Lines changed: 8 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -332,50 +332,24 @@ def attributes(requested_attrs = nil, reload = false)
332332
# @param [JSONAPI::IncludeDirective] include_directive (defaults to the
333333
# +default_include_directive+ config value when not provided)
334334
# @return [Enumerator<Association>]
335-
#
336335
def associations(include_directive = ActiveModelSerializers.default_include_directive, include_slice = nil)
337336
include_slice ||= include_directive
338-
return unless object
337+
return Enumerator.new unless object
339338

340339
Enumerator.new do |y|
341-
self.class._reflections.values.each do |reflection|
340+
self.class._reflections.each do |key, reflection|
342341
next if reflection.excluded?(self)
343-
key = reflection.options.fetch(:key, reflection.name)
344342
next unless include_directive.key?(key)
345343

346-
y.yield reflection.build_association(self, instance_options, include_slice)
344+
association = reflection.build_association(self, instance_options, include_slice)
345+
y.yield association
347346
end
348347
end
349348
end
350349

351350
# @return [Hash] containing the attributes and first level
352351
# associations, similar to how ActiveModel::Serializers::JSON is used
353352
# in ActiveRecord::Base.
354-
#
355-
# TODO: Include <tt>ActiveModel::Serializers::JSON</tt>.
356-
# So that the below is true:
357-
# @param options [nil, Hash] The same valid options passed to `serializable_hash`
358-
# (:only, :except, :methods, and :include).
359-
#
360-
# See
361-
# https://github.com/rails/rails/blob/v5.0.0.beta2/activemodel/lib/active_model/serializers/json.rb#L17-L101
362-
# https://github.com/rails/rails/blob/v5.0.0.beta2/activemodel/lib/active_model/serialization.rb#L85-L123
363-
# https://github.com/rails/rails/blob/v5.0.0.beta2/activerecord/lib/active_record/serialization.rb#L11-L17
364-
# https://github.com/rails/rails/blob/v5.0.0.beta2/activesupport/lib/active_support/core_ext/object/json.rb#L147-L162
365-
#
366-
# @example
367-
# # The :only and :except options can be used to limit the attributes included, and work
368-
# # similar to the attributes method.
369-
# serializer.as_json(only: [:id, :name])
370-
# serializer.as_json(except: [:id, :created_at, :age])
371-
#
372-
# # To include the result of some method calls on the model use :methods:
373-
# serializer.as_json(methods: :permalink)
374-
#
375-
# # To include associations use :include:
376-
# serializer.as_json(include: :posts)
377-
# # Second level and higher order associations work as well:
378-
# serializer.as_json(include: { posts: { include: { comments: { only: :body } }, only: :title } })
379353
def serializable_hash(adapter_options = nil, options = {}, adapter_instance = self.class.serialization_adapter_instance)
380354
adapter_options ||= {}
381355
options[:include_directive] ||= ActiveModel::Serializer.include_directive_from_options(adapter_options)
@@ -387,13 +361,6 @@ def serializable_hash(adapter_options = nil, options = {}, adapter_instance = se
387361
alias to_h serializable_hash
388362

389363
# @see #serializable_hash
390-
# TODO: When moving attributes adapter logic here, @see #serializable_hash
391-
# So that the below is true:
392-
# @param options [nil, Hash] The same valid options passed to `as_json`
393-
# (:root, :only, :except, :methods, and :include).
394-
# The default for `root` is nil.
395-
# The default value for include_root is false. You can change it to true if the given
396-
# JSON string includes a single root node.
397364
def as_json(adapter_opts = nil)
398365
serializable_hash(adapter_opts)
399366
end
@@ -424,14 +391,12 @@ def attributes_hash(_adapter_options, options, adapter_instance)
424391

425392
# @api private
426393
def associations_hash(adapter_options, options, adapter_instance)
427-
relationships = {}
428394
include_directive = options.fetch(:include_directive)
429-
associations(include_directive).each do |association|
430-
adapter_opts = adapter_options.merge(include_directive: include_directive[association.key])
431-
relationships[association.key] ||= association.serializable_hash(adapter_opts, adapter_instance)
395+
include_slice = options[:include_slice]
396+
associations(include_directive, include_slice).each_with_object({}) do |association, relationships|
397+
adapter_opts = adapter_options.merge(include_directive: include_directive[association.key], adapter_instance: adapter_instance)
398+
relationships[association.key] = association.serializable_hash(adapter_opts, adapter_instance)
432399
end
433-
434-
relationships
435400
end
436401

437402
protected
Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,67 @@
1+
require 'active_model/serializer/lazy_association'
2+
13
module ActiveModel
24
class Serializer
35
# This class holds all information about serializer's association.
46
#
5-
# @attr [Symbol] name
6-
# @attr [Hash{Symbol => Object}] options
7-
# @attr [block]
8-
#
9-
# @example
10-
# Association.new(:comments, { serializer: CommentSummarySerializer })
11-
#
12-
class Association < Field
7+
# @api private
8+
Association = Struct.new(:reflection, :association_options) do
9+
attr_reader :lazy_association
10+
delegate :object, :include_data?, :virtual_value, :collection?, to: :lazy_association
11+
12+
def initialize(*)
13+
super
14+
@lazy_association = LazyAssociation.new(reflection, association_options)
15+
end
16+
17+
# @return [Symbol]
18+
delegate :name, to: :reflection
19+
1320
# @return [Symbol]
1421
def key
15-
options.fetch(:key, name)
22+
reflection_options.fetch(:key, name)
1623
end
1724

18-
# @return [ActiveModel::Serializer, nil]
19-
def serializer
20-
options[:serializer]
25+
# @return [True,False]
26+
def key?
27+
reflection_options.key?(:key)
2128
end
2229

2330
# @return [Hash]
2431
def links
25-
options.fetch(:links) || {}
32+
reflection_options.fetch(:links) || {}
2633
end
2734

2835
# @return [Hash, nil]
36+
# This gets mutated, so cannot use the cached reflection_options
2937
def meta
30-
options[:meta]
38+
reflection.options[:meta]
39+
end
40+
41+
def polymorphic?
42+
true == reflection_options[:polymorphic]
3143
end
3244

3345
# @api private
3446
def serializable_hash(adapter_options, adapter_instance)
35-
return options[:virtual_value] if options[:virtual_value]
36-
object = serializer && serializer.object
37-
return unless object
47+
association_serializer = lazy_association.serializer
48+
return virtual_value if virtual_value
49+
association_object = association_serializer && association_serializer.object
50+
return unless association_object
3851

39-
serialization = serializer.serializable_hash(adapter_options, {}, adapter_instance)
52+
serialization = association_serializer.serializable_hash(adapter_options, {}, adapter_instance)
4053

41-
if options[:polymorphic] && serialization
42-
polymorphic_type = object.class.name.underscore
54+
if polymorphic? && serialization
55+
polymorphic_type = association_object.class.name.underscore
4356
serialization = { type: polymorphic_type, polymorphic_type.to_sym => serialization }
4457
end
4558

4659
serialization
4760
end
61+
62+
private
63+
64+
delegate :reflection_options, to: :lazy_association
4865
end
4966
end
5067
end
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module ActiveModel
22
class Serializer
33
# @api private
4-
class BelongsToReflection < SingularReflection
4+
class BelongsToReflection < Reflection
55
end
66
end
77
end

lib/active_model/serializer/collection_reflection.rb

Lines changed: 0 additions & 7 deletions
This file was deleted.

lib/active_model/serializer/concerns/caching.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,14 @@ def object_cache_keys(collection_serializer, adapter_instance, include_directive
193193
cache_keys << object_cache_key(serializer, adapter_instance)
194194

195195
serializer.associations(include_directive).each do |association|
196-
if association.serializer.respond_to?(:each)
197-
association.serializer.each do |sub_serializer|
196+
# TODO(BF): Process relationship without evaluating lazy_association
197+
association_serializer = association.lazy_association.serializer
198+
if association_serializer.respond_to?(:each)
199+
association_serializer.each do |sub_serializer|
198200
cache_keys << object_cache_key(sub_serializer, adapter_instance)
199201
end
200202
else
201-
cache_keys << object_cache_key(association.serializer, adapter_instance)
203+
cache_keys << object_cache_key(association_serializer, adapter_instance)
202204
end
203205
end
204206
end
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
module ActiveModel
22
class Serializer
33
# @api private
4-
class HasManyReflection < CollectionReflection
4+
class HasManyReflection < Reflection
5+
def collection?
6+
true
7+
end
58
end
69
end
710
end
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module ActiveModel
22
class Serializer
33
# @api private
4-
class HasOneReflection < SingularReflection
4+
class HasOneReflection < Reflection
55
end
66
end
77
end
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
module ActiveModel
2+
class Serializer
3+
# @api private
4+
LazyAssociation = Struct.new(:reflection, :association_options) do
5+
REFLECTION_OPTIONS = %i(key links polymorphic meta serializer virtual_value namespace).freeze
6+
7+
delegate :collection?, to: :reflection
8+
9+
def reflection_options
10+
@reflection_options ||= reflection.options.dup.reject { |k, _| !REFLECTION_OPTIONS.include?(k) }
11+
end
12+
13+
def object
14+
@object ||= reflection.value(
15+
association_options.fetch(:parent_serializer),
16+
association_options.fetch(:include_slice)
17+
)
18+
end
19+
alias_method :eval_reflection_block, :object
20+
21+
def include_data?
22+
eval_reflection_block if reflection.block
23+
reflection.include_data?(
24+
association_options.fetch(:include_slice)
25+
)
26+
end
27+
28+
# @return [ActiveModel::Serializer, nil]
29+
def serializer
30+
return @serializer if defined?(@serializer)
31+
if serializer_class
32+
serialize_object!(object)
33+
elsif !object.nil? && !object.instance_of?(Object)
34+
cached_result[:virtual_value] = object
35+
end
36+
@serializer = cached_result[:serializer]
37+
end
38+
39+
def virtual_value
40+
cached_result[:virtual_value] || reflection_options[:virtual_value]
41+
end
42+
43+
def serializer_class
44+
return @serializer_class if defined?(@serializer_class)
45+
serializer_for_options = { namespace: namespace }
46+
serializer_for_options[:serializer] = reflection_options[:serializer] if reflection_options.key?(:serializer)
47+
@serializer_class = association_options.fetch(:parent_serializer).class.serializer_for(object, serializer_for_options)
48+
end
49+
50+
private
51+
52+
def cached_result
53+
@cached_result ||= {}
54+
end
55+
56+
def serialize_object!(object)
57+
if collection?
58+
if (serializer = instantiate_collection_serializer(object)).nil?
59+
# BUG: per #2027, JSON API resource relationships are only id and type, and hence either
60+
# *require* a serializer or we need to be a little clever about figuring out the id/type.
61+
# In either case, returning the raw virtual value will almost always be incorrect.
62+
#
63+
# Should be reflection_options[:virtual_value] or adapter needs to figure out what to do
64+
# with an object that is non-nil and has no defined serializer.
65+
cached_result[:virtual_value] = object.try(:as_json) || object
66+
else
67+
cached_result[:serializer] = serializer
68+
end
69+
else
70+
cached_result[:serializer] = instantiate_serializer(object)
71+
end
72+
end
73+
74+
def instantiate_serializer(object)
75+
serializer_options = association_options.fetch(:parent_serializer_options).except(:serializer)
76+
serializer_options[:serializer_context_class] = association_options.fetch(:parent_serializer).class
77+
serializer = reflection_options.fetch(:serializer, nil)
78+
serializer_options[:serializer] = serializer if serializer
79+
serializer_class.new(object, serializer_options)
80+
end
81+
82+
def instantiate_collection_serializer(object)
83+
serializer = catch(:no_serializer) do
84+
instantiate_serializer(object)
85+
end
86+
serializer
87+
end
88+
89+
def namespace
90+
reflection_options[:namespace] ||
91+
association_options.fetch(:parent_serializer_options)[:namespace]
92+
end
93+
end
94+
end
95+
end

0 commit comments

Comments
 (0)