From ee3cec3d0c0d4da6bbf92b428c022a9b1058ba91 Mon Sep 17 00:00:00 2001 From: Jo Liss Date: Mon, 29 Oct 2012 22:06:50 +0100 Subject: [PATCH] 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. --- lib/active_model/serializer.rb | 18 +++++++++------- lib/active_model/serializer/associations.rb | 11 +++++++--- test/association_test.rb | 23 +++++++++++++++++++++ test/serializer_test.rb | 6 ++++-- 4 files changed, 45 insertions(+), 13 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 03a8fa79..74374455 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -310,7 +310,7 @@ module ActiveModel if association.embed_in_root? && hash.nil? raise IncludeError.new(self.class, association.name) 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 elsif association.embed_objects? 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 # avoids the need to scan through the Array looking for entries every time # we want to merge a new list of values. - def merge_association(hash, key, value, unique_values) - if current_value = unique_values[key] - current_value.merge! value - hash[key] = current_value.to_a - elsif value - hash[key] = value - unique_values[key] = OrderedSet.new(value) + def merge_association(hash, key, serializables, unique_values) + already_serialized = (unique_values[key] ||= {}) + serializable_hashes = (hash[key] ||= []) + + serializables.each do |serializable| + unless already_serialized.include? serializable.object + already_serialized[serializable.object] = true + serializable_hashes << serializable.serializable_hash + end end end diff --git a/lib/active_model/serializer/associations.rb b/lib/active_model/serializer/associations.rb index 64c68b20..74afc1ca 100644 --- a/lib/active_model/serializer/associations.rb +++ b/lib/active_model/serializer/associations.rb @@ -99,7 +99,12 @@ module ActiveModel find_serializable(item).serializable_hash end end - alias serialize_many serialize + + def serializables + associated_object.map do |item| + find_serializable(item) + end + end def serialize_ids # Use pluck or select_columns if available @@ -149,9 +154,9 @@ module ActiveModel end end - def serialize_many + def serializables object = associated_object - value = object && find_serializable(object).serializable_hash + value = object && find_serializable(object) value ? [value] : [] end diff --git a/test/association_test.rb b/test/association_test.rb index a87e9936..f766b5ab 100644 --- a/test/association_test.rb +++ b/test/association_test.rb @@ -284,6 +284,29 @@ class AssociationTest < ActiveModel::TestCase ] }, @root_hash) 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 class InclusionTest < AssociationTest diff --git a/test/serializer_test.rb b/test/serializer_test.rb index 941d371c..1d8e3d69 100644 --- a/test/serializer_test.rb +++ b/test/serializer_test.rb @@ -73,11 +73,13 @@ class SerializerTest < ActiveModel::TestCase class CommentSerializer def initialize(comment, options={}) - @comment = comment + @object = comment end + attr_reader :object + def serializable_hash - { :title => @comment.read_attribute_for_serialization(:title) } + { :title => @object.read_attribute_for_serialization(:title) } end def as_json(options=nil)