Skip to content

Commit 7e9d95a

Browse files
authored
MONGOID-4889 Optimize batch assignment of embedded documents (#6008)
* MONGOID-4889 Optimize batch assignment of embedded documents * rubocop appeasement * simplify some refactoring artifacts * improve the name of the extracted method
1 parent fe9397e commit 7e9d95a

File tree

5 files changed

+114
-15
lines changed

5 files changed

+114
-15
lines changed

.rubocop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Layout/SpaceInsidePercentLiteralDelimiters:
6464
Enabled: false
6565

6666
Metrics/ClassLength:
67-
Max: 200
67+
Enabled: false
6868

6969
Metrics/ModuleLength:
7070
Enabled: false

lib/mongoid/association/embedded/batchable.rb

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -313,18 +313,19 @@ def selector
313313
#
314314
# @return [ Array<Hash> ] The documents as an array of hashes.
315315
def pre_process_batch_insert(docs)
316-
docs.map do |doc|
317-
next unless doc
318-
append(doc)
319-
if persistable? && !_assigning?
320-
self.path = doc.atomic_path unless path
321-
if doc.valid?(:create)
322-
doc.run_before_callbacks(:save, :create)
323-
else
324-
self.inserts_valid = false
316+
[].tap do |results|
317+
append_many(docs) do |doc|
318+
if persistable? && !_assigning?
319+
self.path = doc.atomic_path unless path
320+
if doc.valid?(:create)
321+
doc.run_before_callbacks(:save, :create)
322+
else
323+
self.inserts_valid = false
324+
end
325325
end
326+
327+
results << doc.send(:as_attributes)
326328
end
327-
doc.send(:as_attributes)
328329
end
329330
end
330331

lib/mongoid/association/embedded/embeds_many/proxy.rb

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,67 @@ def append(document)
443443
execute_callback :after_add, document
444444
end
445445

446+
# Returns a unique id for the document, which is either
447+
# its _id or its object_id.
448+
def id_of(doc)
449+
doc._id || doc.object_id
450+
end
451+
452+
# Optimized version of #append that handles multiple documents
453+
# in a more efficient way.
454+
#
455+
# @param [ Array<Document> ] documents The documents to append.
456+
#
457+
# @return [ EmbedsMany::Proxy ] This proxy instance.
458+
def append_many(documents, &block)
459+
unique_set = process_incoming_docs(documents, &block)
460+
461+
_unscoped.concat(unique_set)
462+
_target.push(*scope(unique_set))
463+
update_attributes_hash
464+
465+
unique_set.each { |doc| execute_callback :after_add, doc }
466+
467+
self
468+
end
469+
470+
# Processes the list of documents, building a list of those
471+
# that are not already in the association, and preparing
472+
# each unique document to be integrated into the association.
473+
#
474+
# The :before_add callback is executed for each unique document
475+
# as part of this step.
476+
#
477+
# @param [ Array<Document> ] documents The incoming documents to
478+
# process.
479+
#
480+
# @yield [ Document ] Optional block to call for each unique
481+
# document.
482+
#
483+
# @return [ Array<Document> ] The list of unique documents that
484+
# do not yet exist in the association.
485+
def process_incoming_docs(documents, &block)
486+
visited_docs = Set.new(_target.map { |doc| id_of(doc) })
487+
next_index = _unscoped.size
488+
489+
documents.select do |doc|
490+
next unless doc
491+
492+
id = id_of(doc)
493+
next if visited_docs.include?(id)
494+
495+
execute_callback :before_add, doc
496+
497+
visited_docs.add(id)
498+
integrate(doc)
499+
500+
doc._index = next_index
501+
next_index += 1
502+
503+
block&.call(doc) || true
504+
end
505+
end
506+
446507
# Instantiate the binding associated with this association.
447508
#
448509
# @example Create the binding.

lib/mongoid/association/referenced/has_many/proxy.rb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
# frozen_string_literal: true
22

3-
# TODO: consider refactoring this Proxy class, to satisfy the following
4-
# cops...
5-
# rubocop:disable Metrics/ClassLength
63
module Mongoid
74
module Association
85
module Referenced
@@ -588,4 +585,3 @@ def save_or_delay(doc, docs, inserts)
588585
end
589586
end
590587
end
591-
# rubocop:enable Metrics/ClassLength

spec/integration/associations/embeds_many_spec.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,47 @@
201201
include_examples 'persists correctly'
202202
end
203203
end
204+
205+
context 'including duplicates in the assignment' do
206+
let(:canvas) do
207+
Canvas.create!(shapes: [Shape.new])
208+
end
209+
210+
shared_examples 'persists correctly' do
211+
it 'persists correctly' do
212+
canvas.shapes.length.should eq 2
213+
_canvas = Canvas.find(canvas.id)
214+
_canvas.shapes.length.should eq 2
215+
end
216+
end
217+
218+
context 'via assignment operator' do
219+
before do
220+
canvas.shapes = [ canvas.shapes.first, Shape.new, canvas.shapes.first ]
221+
canvas.save!
222+
end
223+
224+
include_examples 'persists correctly'
225+
end
226+
227+
context 'via attributes=' do
228+
before do
229+
canvas.attributes = { shapes: [ canvas.shapes.first, Shape.new, canvas.shapes.first ] }
230+
canvas.save!
231+
end
232+
233+
include_examples 'persists correctly'
234+
end
235+
236+
context 'via assign_attributes' do
237+
before do
238+
canvas.assign_attributes(shapes: [ canvas.shapes.first, Shape.new, canvas.shapes.first ])
239+
canvas.save!
240+
end
241+
242+
include_examples 'persists correctly'
243+
end
244+
end
204245
end
205246

206247
context 'when an anonymous class defines an embeds_many association' do

0 commit comments

Comments
 (0)