lib/mongoid/relations/referenced/many_to_many.rb in mongoid-2.8.1 vs lib/mongoid/relations/referenced/many_to_many.rb in mongoid-3.0.0.rc
- old
+ new
@@ -1,9 +1,9 @@
# encoding: utf-8
-module Mongoid # :nodoc:
- module Relations #:nodoc:
- module Referenced #:nodoc:
+module Mongoid
+ module Relations
+ module Referenced
# This class defines the behaviour for all relations that are a
# many-to-many between documents in different collections.
class ManyToMany < Many
@@ -23,33 +23,55 @@
#
# @return [ Array<Document> ] The loaded docs.
#
# @since 2.0.0.beta.1
def <<(*args)
- batched do
- [].tap do |ids|
- args.flatten.each do |doc|
- next unless doc
- append(doc)
- if persistable? || _creating?
- ids.push(doc.id)
- doc.save
- else
- base.send(metadata.foreign_key).push(doc.id)
- base.synced[metadata.foreign_key] = false
- end
- end
+ docs = args.flatten
+ return concat(docs) if docs.size > 1
+ if doc = docs.first
+ if !target.include?(doc)
+ append(doc)
+ base.push(foreign_key, doc.id)
if persistable? || _creating?
- base.push_all(metadata.foreign_key, ids)
- base.synced[metadata.foreign_key] = false
+ doc.save
end
end
end
+ unsynced(base, foreign_key) and self
end
- alias :concat :<<
alias :push :<<
+ # Appends an array of documents to the relation. Performs a batch
+ # insert of the documents instead of persisting one at a time.
+ #
+ # @example Concat with other documents.
+ # person.posts.concat([ post_one, post_two ])
+ #
+ # @param [ Array<Document> ] documents The docs to add.
+ #
+ # @return [ Array<Document> ] The documents.
+ #
+ # @since 2.4.0
+ def concat(documents)
+ ids, inserts = [], []
+ documents.each do |doc|
+ next if doc.nil? || target.include?(doc)
+ append(doc)
+ if persistable? || _creating?
+ ids.push(doc.id)
+ save_or_delay(doc, inserts)
+ else
+ base.send(foreign_key).push(doc.id) and unsynced(base, foreign_key)
+ end
+ end
+ if persistable? || _creating?
+ base.push_all(foreign_key, ids)
+ end
+ persist_delayed(inserts)
+ self
+ end
+
# Build a new document from the attributes and append it to this
# relation without saving.
#
# @example Build a new document on the relation.
# person.posts.build(:title => "A new post")
@@ -70,17 +92,17 @@
def build(attributes = {}, options = {}, type = nil)
if options.is_a? Class
options, type = {}, options
end
- Factory.build(type || klass, attributes, options).tap do |doc|
- base.send(metadata.foreign_key).push(doc.id)
- append(doc)
- doc.apply_proc_defaults
- doc.synced[metadata.inverse_foreign_key] = false
- yield(doc) if block_given?
- end
+ doc = Factory.build(type || klass, attributes, options)
+ base.send(foreign_key).push(doc.id)
+ append(doc)
+ doc.apply_post_processed_defaults
+ unsynced(doc, inverse_foreign_key)
+ yield(doc) if block_given?
+ doc
end
alias :new :build
# Delete the document from the relation. This will set the foreign key
# on the document to nil. If the dependent options on the relation are
@@ -93,16 +115,16 @@
#
# @return [ Document ] The matching document.
#
# @since 2.1.0
def delete(document)
- super.tap do |doc|
- if doc && persistable?
- base.pull(metadata.foreign_key, doc.id)
- base.synced[metadata.foreign_key] = false
- end
+ doc = super
+ if doc && persistable?
+ base.pull(foreign_key, doc.id)
+ unsynced(base, foreign_key)
end
+ doc
end
# Removes all associations between the base document and the target
# documents by deleting the foreign keys and the references, orphaning
# the target documents in the process.
@@ -111,16 +133,16 @@
# person.preferences.nullify
#
# @since 2.0.0.rc.1
def nullify
unless metadata.forced_nil_inverse?
- criteria.pull(metadata.inverse_foreign_key, base.id)
+ criteria.pull(inverse_foreign_key, base.id)
end
if persistable?
base.set(
- metadata.foreign_key,
- base.send(metadata.foreign_key).clear
+ foreign_key,
+ base.send(foreign_key).clear
)
end
target.clear do |doc|
unbind_one(doc)
end
@@ -140,14 +162,13 @@
#
# @return [ Many ] The relation.
#
# @since 2.0.0.rc.1
def substitute(replacement)
- tap do |proxy|
- proxy.purge
- proxy.push(replacement.compact.uniq) unless replacement.blank?
- end
+ purge
+ push(replacement.compact.uniq) unless replacement.blank?
+ self
end
# Get a criteria for the documents without the default scoping
# applied.
#
@@ -156,11 +177,11 @@
#
# @return [ Criteria ] The unscoped criteria.
#
# @since 2.4.0
def unscoped
- klass.unscoped.any_in(:_id => base.send(metadata.foreign_key))
+ klass.unscoped.any_in(_id: base.send(foreign_key))
end
private
# Appends the document to the target array, updating the index on the
@@ -196,13 +217,31 @@
# @example Get a criteria for the relation.
# relation.criteria
#
# @return [ Criteria ] A new criteria.
def criteria
- ManyToMany.criteria(metadata, base.send(metadata.foreign_key))
+ ManyToMany.criteria(metadata, base.send(foreign_key))
end
+ # Flag the base as unsynced with respect to the foreign key.
+ #
+ # @api private
+ #
+ # @example Flag as unsynced.
+ # relation.unsynced(doc, :preference_ids)
+ #
+ # @param [ Document ] doc The document to flag.
+ # @param [ Symbol ] key The key to flag on the document.
+ #
+ # @return [ true ] true.
+ #
+ # @since 3.0.0
+ def unsynced(doc, key)
+ doc.synced[key] = false
+ true
+ end
+
class << self
# Return the builder that is responsible for generating the documents
# that will be used by this relation.
#
@@ -233,27 +272,29 @@
#
# @return [ Criteria ] The criteria.
#
# @since 2.1.0
def criteria(metadata, object, type = nil)
- metadata.klass.any_in(:_id => object)
+ metadata.klass.all_of(_id: { "$in" => object })
end
# Get the criteria that is used to eager load a relation of this
# type.
#
# @example Get the eager load criteria.
# Proxy.eager_load(metadata, criteria)
#
# @param [ Metadata ] metadata The relation metadata.
- # @param [ Criteria ] criteria The criteria being used.
+ # @param [ Array<Object> ] ids The ids of the documents to load.
#
# @return [ Criteria ] The criteria to eager load the relation.
#
# @since 2.2.0
- def eager_load(metadata, criteria)
- raise Errors::EagerLoad.new(metadata.name)
+ def eager_load(metadata, ids)
+ metadata.klass.any_in(_id: ids).each do |doc|
+ IdentityMap.set(doc)
+ end
end
# Returns true if the relation is an embedded one. In this case
# always false.
#
@@ -265,10 +306,24 @@
# @since 2.0.0.rc.1
def embedded?
false
end
+ # Get the foreign key for the provided name.
+ #
+ # @example Get the foreign key.
+ # Referenced::ManyToMany.foreign_key(:person)
+ #
+ # @param [ Symbol ] name The name.
+ #
+ # @return [ String ] The foreign key.
+ #
+ # @since 3.0.0
+ def foreign_key(name)
+ "#{name.to_s.singularize}#{foreign_key_suffix}"
+ end
+
# Get the default value for the foreign key.
#
# @example Get the default.
# Referenced::ManyToMany.foreign_key_default
#
@@ -295,12 +350,12 @@
# reflection.
#
# @example Get the macro.
# Referenced::ManyToMany.macro
#
- # @return [ Symbol ] :references_and_referenced_in_many
+ # @return [ Symbol ] :has_and_belongs_to_many
def macro
- :references_and_referenced_in_many
+ :has_and_belongs_to_many
end
# Return the nested builder that is responsible for generating the documents
# that will be used by this relation.
#