When objects are sideloaded multiple times, serialize them only once

To achieve this, we make the following change when sideloading: Instead
of serializing associations and discarding duplicate *hashes*, we
memorize the *objects* (records) that we have already serialized, and
only serialize those that are new.

This change is mostly transparent, and brings down serialization time
from 3.1 seconds to 1.0 seconds on my set of sample data.

There is one change in the behavior: If you sideload the same object
multiple times, and it yields different hashes, like so:

    embed :ids, include: true
    has_many :comments
    has_many :recent_comments, root: comments, serializer: CommentShortSerializer

then previously, it would be included multiple times, whereas now, the
first hash wins. (I haven't actually tested this.) I don't know that
either option is preferable. It's not covered by the test suite, and I
think it's an edge case that is OK to ignore entirely.
This commit is contained in:
Jo Liss 2012-10-29 22:06:50 +01:00
parent 28ee88ca9a
commit ee3cec3d0c
4 changed files with 45 additions and 13 deletions

View File

@ -310,7 +310,7 @@ module ActiveModel
if association.embed_in_root? && hash.nil? if association.embed_in_root? && hash.nil?
raise IncludeError.new(self.class, association.name) raise IncludeError.new(self.class, association.name)
elsif association.embed_in_root? && association.embeddable? elsif association.embed_in_root? && association.embeddable?
merge_association hash, association.root, association.serialize_many, unique_values merge_association hash, association.root, association.serializables, unique_values
end end
elsif association.embed_objects? elsif association.embed_objects?
node[association.key] = association.serialize node[association.key] = association.serialize
@ -326,13 +326,15 @@ module ActiveModel
# a unique list of all of the objects that are already in the Array. This # a unique list of all of the objects that are already in the Array. This
# avoids the need to scan through the Array looking for entries every time # avoids the need to scan through the Array looking for entries every time
# we want to merge a new list of values. # we want to merge a new list of values.
def merge_association(hash, key, value, unique_values) def merge_association(hash, key, serializables, unique_values)
if current_value = unique_values[key] already_serialized = (unique_values[key] ||= {})
current_value.merge! value serializable_hashes = (hash[key] ||= [])
hash[key] = current_value.to_a
elsif value serializables.each do |serializable|
hash[key] = value unless already_serialized.include? serializable.object
unique_values[key] = OrderedSet.new(value) already_serialized[serializable.object] = true
serializable_hashes << serializable.serializable_hash
end
end end
end end

View File

@ -99,7 +99,12 @@ module ActiveModel
find_serializable(item).serializable_hash find_serializable(item).serializable_hash
end end
end end
alias serialize_many serialize
def serializables
associated_object.map do |item|
find_serializable(item)
end
end
def serialize_ids def serialize_ids
# Use pluck or select_columns if available # Use pluck or select_columns if available
@ -149,9 +154,9 @@ module ActiveModel
end end
end end
def serialize_many def serializables
object = associated_object object = associated_object
value = object && find_serializable(object).serializable_hash value = object && find_serializable(object)
value ? [value] : [] value ? [value] : []
end end

View File

@ -284,6 +284,29 @@ class AssociationTest < ActiveModel::TestCase
] ]
}, @root_hash) }, @root_hash)
end end
def test_embed_ids_include_true_does_not_serialize_multiple_times
@post.recent_comment = @comment
@post_serializer_class.class_eval do
has_one :comment, :embed => :ids, :include => true
has_one :recent_comment, :embed => :ids, :include => true, :root => :comments
end
# Count how often the @comment record is serialized.
serialized_times = 0
@comment.class_eval do
define_method :read_attribute_for_serialization, lambda { |name|
serialized_times += 1 if name == :body
super(name)
}
end
include_bare! :comment
include_bare! :recent_comment
assert_equal 1, serialized_times
end
end end
class InclusionTest < AssociationTest class InclusionTest < AssociationTest

View File

@ -73,11 +73,13 @@ class SerializerTest < ActiveModel::TestCase
class CommentSerializer class CommentSerializer
def initialize(comment, options={}) def initialize(comment, options={})
@comment = comment @object = comment
end end
attr_reader :object
def serializable_hash def serializable_hash
{ :title => @comment.read_attribute_for_serialization(:title) } { :title => @object.read_attribute_for_serialization(:title) }
end end
def as_json(options=nil) def as_json(options=nil)