lib/mongoid/relations/embedded/many.rb
# encoding: utf-8
require "mongoid/relations/embedded/batchable"
module Mongoid
module Relations
module Embedded
# This class handles the behaviour for a document that embeds many other
# documents within in it as an array.
class Many < Relations::Many
include Batchable
# 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.addresses << address
#
# @example Push a document.
# person.addresses.push(address)
#
# @param [ Document, Array<Document> ] *args Any number of documents.
def <<(*args)
docs = args.flatten
return concat(docs) if docs.size > 1
if doc = docs.first
append(doc)
doc.save if persistable? && !_assigning?
end
self
end
alias :push :<<
# Get this relation as as its representation in the database.
#
# @example Convert the relation to an attributes hash.
# person.addresses.as_document
#
# @return [ Array<Hash> ] The relation as stored in the db.
#
# @since 2.0.0.rc.1
def as_document
attributes = []
_unscoped.each do |doc|
attributes.push(doc.as_document)
end
attributes
end
# 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.addresses.concat([ address_one, address_two ])
#
# @param [ Array<Document> ] docs The docs to add.
#
# @return [ Array<Document> ] The documents.
#
# @since 2.4.0
def concat(docs)
batch_insert(docs) unless docs.empty?
self
end
# Builds a new document in the relation and appends it to the target.
# Takes an optional type if you want to specify a subclass.
#
# @example Build a new document on the relation.
# person.people.build(:name => "Bozo")
#
# @overload build(attributes = {}, options = {}, type = nil)
# @param [ Hash ] attributes The attributes to build the document with.
# @param [ Class ] type Optional class to build the document with.
#
# @overload build(attributes = {}, type = nil)
# @param [ Hash ] attributes The attributes to build the document with.
# @param [ Class ] type Optional class to build the document with.
#
# @return [ Document ] The new document.
def build(attributes = {}, type = nil)
doc = Factory.build(type || __metadata.klass, attributes)
append(doc)
doc.apply_post_processed_defaults
yield(doc) if block_given?
doc.run_callbacks(:build) { doc }
base._reset_memoized_children!
doc
end
alias :new :build
# Clear the relation. Will delete the documents from the db if they are
# already persisted.
#
# @example Clear the relation.
# person.addresses.clear
#
# @return [ Many ] The empty relation.
def clear
batch_clear(target.dup)
self
end
# Returns a count of the number of documents in the association that have
# actually been persisted to the database.
#
# Use #size if you want the total number of documents.
#
# @example Get the count of persisted documents.
# person.addresses.count
#
# @return [ Integer ] The total number of persisted embedded docs, as
# flagged by the #persisted? method.
def count
target.select { |doc| doc.persisted? }.size
end
# Delete the supplied document from the target. This method is proxied
# in order to reindex the array after the operation occurs.
#
# @example Delete the document from the relation.
# person.addresses.delete(address)
#
# @param [ Document ] document The document to be deleted.
#
# @return [ Document, nil ] The deleted document or nil if nothing deleted.
#
# @since 2.0.0.rc.1
def delete(document)
execute_callback :before_remove, document
doc = target.delete_one(document)
if doc && !_binding?
_unscoped.delete_one(doc)
if _assigning?
base.add_atomic_pull(doc)
else
doc.delete(suppress: true)
unbind_one(doc)
end
end
reindex
execute_callback :after_remove, document
doc
end
# Delete all the documents in the association without running callbacks.
#
# @example Delete all documents from the relation.
# person.addresses.delete_all
#
# @example Conditionally delete documents from the relation.
# person.addresses.delete_all({ :street => "Bond" })
#
# @param [ Hash ] conditions Conditions on which documents to delete.
#
# @return [ Integer ] The number of documents deleted.
def delete_all(conditions = {})
remove_all(conditions, :delete)
end
# Delete all the documents for which the provided block returns true.
#
# @example Delete the matching documents.
# person.addresses.delete_if do |doc|
# doc.state == "GA"
# end
#
# @return [ Many, Enumerator ] The relation or an enumerator if no
# block was provided.
#
# @since 3.1.0
def delete_if
if block_given?
dup_target = target.dup
dup_target.each do |doc|
delete(doc) if yield(doc)
end
self
else
super
end
end
# Destroy all the documents in the association whilst running callbacks.
#
# @example Destroy all documents from the relation.
# person.addresses.destroy_all
#
# @example Conditionally destroy documents from the relation.
# person.addresses.destroy_all({ :street => "Bond" })
#
# @param [ Hash ] conditions Conditions on which documents to destroy.
#
# @return [ Integer ] The number of documents destroyed.
def destroy_all(conditions = {})
remove_all(conditions, :destroy)
end
# Determine if any documents in this relation exist in the database.
#
# @example Are there persisted documents?
# person.posts.exists?
#
# @return [ true, false ] True is persisted documents exist, false if not.
def exists?
count > 0
end
# Finds a document in this association through several different
# methods.
#
# @example Find a document by its id.
# person.addresses.find(BSON::ObjectId.new)
#
# @example Find documents for multiple ids.
# person.addresses.find([ BSON::ObjectId.new, BSON::ObjectId.new ])
#
# @param [ Array<Object> ] args Various arguments.
#
# @return [ Array<Document>, Document ] A single or multiple documents.
def find(*args)
criteria.find(*args)
end
# Instantiate a new embeds_many relation.
#
# @example Create the new relation.
# Many.new(person, addresses, metadata)
#
# @param [ Document ] base The document this relation hangs off of.
# @param [ Array<Document> ] target The child documents of the relation.
# @param [ Metadata ] metadata The relation's metadata
#
# @return [ Many ] The proxy.
def initialize(base, target, metadata)
init(base, target, metadata) do
target.each_with_index do |doc, index|
integrate(doc)
doc._index = index
end
@_unscoped = target.dup
@target = scope(target)
end
end
# Get all the documents in the relation that are loaded into memory.
#
# @example Get the in memory documents.
# relation.in_memory
#
# @return [ Array<Document> ] The documents in memory.
#
# @since 2.1.0
def in_memory
target
end
# Pop documents off the relation. This can be a single document or
# multiples, and will automatically persist the changes.
#
# @example Pop a single document.
# relation.pop
#
# @example Pop multiple documents.
# relation.pop(3)
#
# @param [ Integer ] count The number of documents to pop, or 1 if not
# provided.
#
# @return [ Document, Array<Document> ] The popped document(s).
#
# @since 3.0.0
def pop(count = nil)
if count
if docs = target[target.size - count, target.size]
docs.each { |doc| delete(doc) }
end
else
delete(target[-1])
end
end
# Substitutes the supplied target documents for the existing documents
# in the relation.
#
# @example Substitute the relation's target.
# person.addresses.substitute([ address ])
#
# @param [ Array<Document> ] docs The replacement docs.
#
# @return [ Many ] The proxied relation.
#
# @since 2.0.0.rc.1
def substitute(docs)
batch_replace(docs)
self
end
# Return the relation with all previous scoping removed. This is the
# exact representation of the docs in the database.
#
# @example Get the unscoped documents.
# person.addresses.unscoped
#
# @return [ Criteria ] The unscoped relation.
#
# @since 2.4.0
def unscoped
criterion = klass.unscoped
criterion.embedded = true
criterion.documents = _unscoped.delete_if(&:marked_for_destruction?)
criterion
end
private
# Appends the document to the target array, updating the index on the
# document at the same time.
#
# @example Append to the document.
# relation.append(document)
#
# @param [ Document ] document The document to append to the target.
#
# @since 2.0.0.rc.1
def append(document)
execute_callback :before_add, document
target.push(*scope([document]))
_unscoped.push(document)
integrate(document)
document._index = _unscoped.size - 1
execute_callback :after_add, document
end
# Instantiate the binding associated with this relation.
#
# @example Create the binding.
# relation.binding([ address ])
#
# @param [ Array<Document> ] new_target The new documents to bind with.
#
# @return [ Binding ] The many binding.
#
# @since 2.0.0.rc.1
def binding
Bindings::Embedded::Many.new(base, target, __metadata)
end
# Returns the criteria object for the target class with its documents set
# to target.
#
# @example Get a criteria for the relation.
# relation.criteria
#
# @return [ Criteria ] A new criteria.
def criteria
criterion = klass.scoped
criterion.embedded = true
criterion.documents = target
criterion.parent_document = base
criterion.metadata = relation_metadata
Many.apply_ordering(criterion, __metadata)
end
# Deletes one document from the target and unscoped.
#
# @api private
#
# @example Delete one document.
# relation.delete_one(doc)
#
# @param [ Document ] document The document to delete.
#
# @since 2.4.7
def delete_one(document)
target.delete_one(document)
_unscoped.delete_one(document)
reindex
end
# Integrate the document into the relation. will set its metadata and
# attempt to bind the inverse.
#
# @example Integrate the document.
# relation.integrate(document)
#
# @param [ Document ] document The document to integrate.
#
# @since 2.1.0
def integrate(document)
characterize_one(document)
bind_one(document)
end
# If the target array does not respond to the supplied method then try to
# find a named scope or criteria on the class and send the call there.
#
# If the method exists on the array, use the default proxy behavior.
#
# @param [ Symbol, String ] name The name of the method.
# @param [ Array ] args The method args
# @param [ Proc ] block Optional block to pass.
#
# @return [ Criteria, Object ] A Criteria or return value from the target.
def method_missing(name, *args, &block)
return super if target.respond_to?(name)
klass.send(:with_scope, criteria) do
criteria.public_send(name, *args, &block)
end
end
# Are we able to persist this relation?
#
# @example Can we persist the relation?
# relation.persistable?
#
# @return [ true, false ] If the relation is persistable.
#
# @since 2.1.0
def persistable?
base.persisted? && !_binding?
end
# Reindex all the target elements. This is useful when performing
# operations on the proxied target directly and the indices need to
# match that on the database side.
#
# @example Reindex the relation.
# person.addresses.reindex
#
# @since 2.0.0.rc.1
def reindex
_unscoped.each_with_index do |doc, index|
doc._index = index
end
end
# Apply the metadata ordering or the default scoping to the provided
# documents.
#
# @example Apply scoping.
# person.addresses.scope(target)
#
# @param [ Array<Document> ] docs The documents to scope.
#
# @return [ Array<Document> ] The scoped docs.
#
# @since 2.4.0
def scope(docs)
return docs unless __metadata.order || __metadata.klass.default_scoping?
crit = __metadata.klass.order_by(__metadata.order)
crit.embedded = true
crit.documents = docs
crit.entries
end
# Remove all documents from the relation, either with a delete or a
# destroy depending on what this was called through.
#
# @example Destroy documents from the relation.
# relation.remove_all({ :num => 1 }, true)
#
# @param [ Hash ] conditions Conditions to filter by.
# @param [ true, false ] destroy If true then destroy, else delete.
#
# @return [ Integer ] The number of documents removed.
def remove_all(conditions = {}, method = :delete)
criteria = where(conditions || {})
removed = criteria.size
batch_remove(criteria, method)
removed
end
# Get the internal unscoped documents.
#
# @example Get the unscoped documents.
# relation._unscoped
#
# @return [ Array<Document> ] The unscoped documents.
#
# @since 2.4.0
def _unscoped
@_unscoped ||= []
end
# Set the internal unscoped documents.
#
# @example Set the unscoped documents.
# relation._unscoped = docs
#
# @param [ Array<Document> ] docs The documents.
#
# @return [ Array<Document ] The unscoped docs.
#
# @since 2.4.0
def _unscoped=(docs)
@_unscoped = docs
end
class << self
# Return the builder that is responsible for generating the documents
# that will be used by this relation.
#
# @example Get the builder.
# Embedded::Many.builder(meta, object)
#
# @param [ Document ] base The base document.
# @param [ Metadata ] meta The metadata of the relation.
# @param [ Document, Hash ] object A document or attributes to build
# with.
#
# @return [ Builder ] A newly instantiated builder object.
#
# @since 2.0.0.rc.1
def builder(base, meta, object)
Builders::Embedded::Many.new(base, meta, object)
end
# Returns true if the relation is an embedded one. In this case
# always true.
#
# @example Is the relation embedded?
# Embedded::Many.embedded?
#
# @return [ true ] true.
#
# @since 2.0.0.rc.1
def embedded?
true
end
# Returns the suffix of the foreign key field, either "_id" or "_ids".
#
# @example Get the suffix for the foreign key.
# Referenced::Many.foreign_key_suffix
#
# @return [ nil ] nil.
#
# @since 3.0.0
def foreign_key_suffix
nil
end
# Returns the macro for this relation. Used mostly as a helper in
# reflection.
#
# @example Get the relation macro.
# Mongoid::Relations::Embedded::Many.macro
#
# @return [ Symbol ] :embeds_many
#
# @since 2.0.0.rc.1
def macro
:embeds_many
end
# Return the nested builder that is responsible for generating the
# documents that will be used by this relation.
#
# @example Get the nested builder.
# NestedAttributes::Many.builder(attributes, options)
#
# @param [ Metadata ] metadata The relation metadata.
# @param [ Hash ] attributes The attributes to build with.
# @param [ Hash ] options The builder options.
#
# @option options [ true, false ] :allow_destroy Can documents be
# deleted?
# @option options [ Integer ] :limit Max number of documents to
# create at once.
# @option options [ Proc, Symbol ] :reject_if If documents match this
# option then they are ignored.
# @option options [ true, false ] :update_only Only existing documents
# can be modified.
#
# @return [ NestedBuilder ] The nested attributes builder.
#
# @since 2.0.0.rc.1
def nested_builder(metadata, attributes, options)
Builders::NestedAttributes::Many.new(metadata, attributes, options)
end
# Get the path calculator for the supplied document.
#
# @example Get the path calculator.
# Proxy.path(document)
#
# @param [ Document ] document The document to calculate on.
#
# @return [ Mongoid::Atomic::Paths::Embedded::Many ]
# The embedded many atomic path calculator.
#
# @since 2.1.0
def path(document)
Mongoid::Atomic::Paths::Embedded::Many.new(document)
end
# Tells the caller if this relation is one that stores the foreign
# key on its own objects.
#
# @example Does this relation store a foreign key?
# Embedded::Many.stores_foreign_key?
#
# @return [ false ] false.
#
# @since 2.0.0.rc.1
def stores_foreign_key?
false
end
# Get the valid options allowed with this relation.
#
# @example Get the valid options.
# Relation.valid_options
#
# @return [ Array<Symbol> ] The valid options.
#
# @since 2.1.0
def valid_options
[
:as, :cascade_callbacks, :cyclic, :order, :store_as,
:before_add, :after_add, :before_remove, :after_remove
]
end
# Get the default validation setting for the relation. Determines if
# by default a validates associated will occur.
#
# @example Get the validation default.
# Proxy.validation_default
#
# @return [ true, false ] The validation default.
#
# @since 2.1.9
def validation_default
true
end
end
end
end
end
end