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. #