Skip to content

Commit 1913bb3

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

File tree

3 files changed

+113
-10
lines changed

3 files changed

+113
-10
lines changed

lib/mongoid/association/embedded/batchable.rb

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

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,67 @@ def append(document)
411411
execute_callback :after_add, document
412412
end
413413

414+
# Returns a unique id for the document, which is either
415+
# its _id or its object_id.
416+
def id_of(doc)
417+
doc._id || doc.object_id
418+
end
419+
420+
# Optimized version of #append that handles multiple documents
421+
# in a more efficient way.
422+
#
423+
# @param [ Array<Document> ] documents The documents to append.
424+
#
425+
# @return [ EmbedsMany::Proxy ] This proxy instance.
426+
def append_many(documents, &block)
427+
unique_set = process_incoming_docs(documents, &block)
428+
429+
_unscoped.concat(unique_set)
430+
_target.push(*scope(unique_set))
431+
update_attributes_hash
432+
433+
unique_set.each { |doc| execute_callback :after_add, doc }
434+
435+
self
436+
end
437+
438+
# Processes the list of documents, building a list of those
439+
# that are not already in the association, and preparing
440+
# each unique document to be integrated into the association.
441+
#
442+
# The :before_add callback is executed for each unique document
443+
# as part of this step.
444+
#
445+
# @param [ Array<Document> ] documents The incoming documents to
446+
# process.
447+
#
448+
# @yield [ Document ] Optional block to call for each unique
449+
# document.
450+
#
451+
# @return [ Array<Document> ] The list of unique documents that
452+
# do not yet exist in the association.
453+
def process_incoming_docs(documents, &block)
454+
visited_docs = Set.new(_target.map { |doc| id_of(doc) })
455+
next_index = _unscoped.size
456+
457+
documents.select do |doc|
458+
next unless doc
459+
460+
id = id_of(doc)
461+
next if visited_docs.include?(id)
462+
463+
execute_callback :before_add, doc
464+
465+
visited_docs.add(id)
466+
integrate(doc)
467+
468+
doc._index = next_index
469+
next_index += 1
470+
471+
block&.call(doc) || true
472+
end
473+
end
474+
414475
# Instantiate the binding associated with this association.
415476
#
416477
# @example Create the binding.

spec/integration/associations/embeds_many_spec.rb

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

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

0 commit comments

Comments
 (0)