Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Adding batch inserts to has_many when appending multiple docs at once

  • Loading branch information...
commit 55a116a696544c5a0da2e7bf816857b431f0de98 1 parent 301f61d
@durran durran authored
View
3  lib/mongoid/collection.rb
@@ -94,10 +94,11 @@ def initialize(klass, name)
# @param [ Hash, Array<Hash> ] documents A single document or multiples.
# @param [ Hash ] options The options.
#
- # @since 2.0.2
+ # @since 2.0.2, batch-relational-insert
def insert(documents, options = {})
inserter = Thread.current[:mongoid_batch_insert]
if inserter
+ inserter.consume(documents, options)
else
master.insert(documents, options)
end
View
1  lib/mongoid/relations.rb
@@ -14,6 +14,7 @@
require "mongoid/relations/embedded/in"
require "mongoid/relations/embedded/many"
require "mongoid/relations/embedded/one"
+require "mongoid/relations/referenced/batch"
require "mongoid/relations/referenced/in"
require "mongoid/relations/referenced/many"
require "mongoid/relations/referenced/many_to_many"
View
71 lib/mongoid/relations/referenced/batch.rb
@@ -0,0 +1,71 @@
+# encoding: utf-8
+require "mongoid/relations/referenced/batch/insert"
+
+module Mongoid #:nodoc:
+ module Relations #:nodoc:
+ module Referenced #:nodoc:
+
+ # This module provides the ability for single insert calls to be batch
+ # inserted.
+ module Batch
+
+ private
+
+ # Executes a number of save calls in a single batch. Mongoid will
+ # intercept all database inserts while in this block and combine them
+ # into a single database call. When the block concludes the batch
+ # insert will occur.
+ #
+ # Since the collection is accessed through the class it would not be
+ # thread safe to give it state so we access the atomic updater via the
+ # current thread.
+ #
+ # @note This operation is not safe when attemping to do illegal updates
+ # for different objects or collections, since the updator is not
+ # scoped on the thread. This is meant for Mongoid internal use only
+ # to keep existing design clean.
+ #
+ # @example Batch update multiple appends.
+ # batched do
+ # person.posts << [ post_one, post_two, post_three ]
+ # end
+ #
+ # @param [ Proc ] block The block to execute.
+ #
+ # @return [ Object ] The result of the operation.
+ #
+ # @since 2.0.2, batch-relational-insert
+ def batched(&block)
+ inserter = Thread.current[:mongoid_batch_insert] ||= Insert.new
+ count_executions do
+ block.call if block
+ end.tap do
+ if @executions.zero?
+ Thread.current[:mongoid_batch_insert] = nil
+ inserter.execute(collection)
+ end
+ end
+ end
+
+ # Execute the block, incrementing the executions before the call and
+ # decrementing them after in order to be able to nest blocks within
+ # each other.
+ #
+ # @todo Durran: Combine common code with embedded atomics.
+ #
+ # @example Execute and increment.
+ # execute { block.call }
+ #
+ # @param [ Proc ] block The block to call.
+ #
+ # @since 2.0.2, batch-relational-insert
+ def count_executions(&block)
+ @executions ||= 0 and @executions += 1
+ block.call.tap do
+ @executions -=1
+ end
+ end
+ end
+ end
+ end
+end
View
57 lib/mongoid/relations/referenced/batch/insert.rb
@@ -0,0 +1,57 @@
+# encoding: utf-8
+module Mongoid #:nodoc:
+ module Relations #:nodoc:
+ module Referenced #:nodoc:
+ module Batch #:nodoc:
+
+ # Handles all the batch insert collection.
+ class Insert
+ attr_accessor :documents, :options
+
+ # Consumes an execution that was supposed to hit the database, but is
+ # now being deferred to later in favor of a single batch insert.
+ #
+ # @example Consume the operation.
+ # set.consume({ "field" => "value" }, { :safe => true })
+ #
+ # @param [ Hash ] document The document to collect.
+ # @param [ Hash ] options The persistence options.
+ #
+ # @option options [ true, false ] :safe Persist in safe mode.
+ #
+ # @since 2.0.2, batch-relational-insert
+ def consume(document, options = {})
+ @consumed, @options = true, options
+ (@documents ||= []).push(document)
+ end
+
+ # Has this operation consumed any executions?
+ #
+ # @example Is this consumed?
+ # insert.consumed?
+ #
+ # @return [ true, false ] If the operation has consumed anything.
+ #
+ # @since 2.0.2, batch-relational-insert
+ def consumed?
+ !!@consumed
+ end
+
+ # Execute the batch insert operation on the collection.
+ #
+ # @example Execute the operation.
+ # insert.execute(collection)
+ #
+ # @param [ Collection ] collection The root collection.
+ #
+ # @since 2.0.2, batch-relational-insert
+ def execute(collection)
+ if collection && consumed?
+ collection.insert(documents, options)
+ end
+ end
+ end
+ end
+ end
+ end
+end
View
39 lib/mongoid/relations/referenced/many.rb
@@ -6,6 +6,33 @@ module Referenced #:nodoc:
# This class defines the behaviour for all relations that are a
# one-to-many between documents in different collections.
class Many < Relations::Many
+ include Batch
+
+ # Appends a document or array of documents to the relation. Will set
+ # the parent and update the index in the process.
+ #
+ # @example Append a document.
+ # person.posts << post
+ #
+ # @example Push a document.
+ # person.posts.push(post)
+ #
+ # @example Concat with other documents.
+ # person.posts.concat([ post_one, post_two ])
+ #
+ # @param [ Document, Array<Document> ] *args Any number of documents.
+ def <<(*args)
+ options = default_options(args)
+ batched do
+ args.flatten.each do |doc|
+ return doc unless doc
+ append(doc, options)
+ doc.save if base.persisted? && !options[:binding]
+ end
+ end
+ end
+ alias :concat :<<
+ alias :push :<<
# Binds the base object to the inverse of the relation. This is so we
# are referenced to the actual objects themselves and dont hit the
@@ -286,6 +313,18 @@ def binding(new_target = nil)
Bindings::Referenced::Many.new(base, new_target || target, metadata)
end
+ # Get the collection of the relation in question.
+ #
+ # @example Get the collection of the relation.
+ # relation.collection
+ #
+ # @return [ Collection ] The collection of the relation.
+ #
+ # @since 2.0.2, batch-relational-insert
+ def collection
+ metadata.klass.collection
+ end
+
# Returns the criteria object for the target class with its documents set
# to target.
#
View
1  spec/spec_helper.rb
@@ -60,4 +60,3 @@
ActiveSupport::Inflector.inflections do |inflect|
inflect.singular("address_components", "address_component")
end
-
View
95 spec/unit/mongoid/relations/referenced/batch/insert_spec.rb
@@ -0,0 +1,95 @@
+require "spec_helper"
+
+describe Mongoid::Relations::Referenced::Batch::Insert do
+
+ describe "#consume" do
+
+ let(:insert) do
+ described_class.new
+ end
+
+ let(:document) do
+ { :field => "value" }
+ end
+
+ let(:options) do
+ { :safe => true }
+ end
+
+ before do
+ insert.consume(document, options)
+ end
+
+ it "sets consumed to true" do
+ insert.should be_consumed
+ end
+
+ it "sets the options" do
+ insert.options.should == options
+ end
+
+ it "appends the document" do
+ insert.documents.should == [ document ]
+ end
+ end
+
+ describe "#consumed?" do
+
+ context "when the operation has been consumed" do
+
+ let(:insert) do
+ described_class.new
+ end
+
+ before do
+ insert.consume({})
+ end
+
+ it "returns true" do
+ insert.should be_consumed
+ end
+ end
+
+ context "when the operation has not been consumed" do
+
+ let(:insert) do
+ described_class.new
+ end
+
+ it "returns false" do
+ insert.should_not be_consumed
+ end
+ end
+ end
+
+ describe "#execute" do
+
+ let(:insert) do
+ described_class.new
+ end
+
+ let(:collection) do
+ stub
+ end
+
+ context "when the operation has been consumed" do
+
+ before do
+ insert.consume({})
+ end
+
+ it "sends the insert to the collection" do
+ collection.expects(:insert).with([{}], {})
+ insert.execute(collection)
+ end
+ end
+
+ context "when the operation has not been comsumed" do
+
+ it "does not send an insert" do
+ collection.expects(:insert).never
+ insert.execute(collection)
+ end
+ end
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.