# encoding: utf-8
module Mongoid #:nodoc:
module Associations #:nodoc:
# Represents an relational one-to-many association with an object in a
# separate collection or database.
class ReferencesMany < Proxy
# Appends the object to the +Array+, setting its parent in
# the process.
def <<(*objects)
load_target
objects.flatten.each do |object|
object.send("#{@foreign_key}=", @parent.id)
@target << object
object.save unless @parent.new_record?
end
end
alias :concat :<<
alias :push :<<
# Builds a new Document and adds it to the association collection. The
# document created will be of the same class as the others in the
# association, and the attributes will be passed into the constructor.
#
# Returns the newly created object.
def build(attributes = nil)
load_target
name = determine_name
object = @klass.instantiate((attributes || {}).merge(name => @parent))
@target << object
object
end
# Creates a new Document and adds it to the association collection. The
# document created will be of the same class as the others in the
# association, and the attributes will be passed into the constructor and
# the new object will then be saved.
#
# Returns the newly created object.
def create(attributes = nil)
build(attributes).tap(&:save)
end
# Creates a new Document and adds it to the association collection. If
# validation fails an error is raised.
#
# Returns the newly created object.
def create!(attributes = nil)
build(attributes).tap(&:save!)
end
# Delete all the associated objects.
#
# Example:
#
# person.posts.delete_all
#
# Returns:
#
# The number of objects deleted.
def delete_all(conditions = {})
remove(:delete_all, conditions[:conditions])
end
# Destroy all the associated objects.
#
# Example:
#
# person.posts.destroy_all
#
# Returns:
#
# The number of objects destroyed.
def destroy_all(conditions = {})
remove(:destroy_all, conditions[:conditions])
end
# Finds a document in this association.
# If an id is passed, will return the document for that id.
def find(id_or_type, options = {})
return self.id_criteria(id_or_type) unless id_or_type.is_a?(Symbol)
options[:conditions] = (options[:conditions] || {}).merge(@foreign_key.to_sym => @parent.id)
@klass.find(id_or_type, options)
end
# Initializing a related association only requires looking up the objects
# by their ids.
#
# Options:
#
# document: The +Document+ that contains the relationship.
# options: The association +Options+.
def initialize(document, options, target = nil)
setup(document, options)
@target = target || query.call
end
# Override the default behavior to allow the criteria to get reset on
# each call into the association.
#
# Example:
#
# person.posts.where(:title => "New")
# person.posts # resets the criteria
#
# Returns:
#
# A Criteria object or Array.
def method_missing(name, *args, &block)
@target = query.call unless @target.is_a?(Array)
@target.send(name, *args, &block)
end
# Used for setting associations via a nested attributes setter from the
# parent +Document+.
#
# Options:
#
# attributes: A +Hash+ of integer keys and +Hash+ values.
#
# Returns:
#
# The newly build target Document.
def nested_build(attributes, options = {})
attributes.each do |index, attrs|
begin
document = find(index.to_i)
if options && options[:allow_destroy] && attrs['_destroy']
@target.delete(document)
document.destroy
else
document.write_attributes(attrs)
end
rescue Errors::DocumentNotFound
build(attrs)
end
end
end
protected
# Load the target entries if the parent document is new.
def load_target
@target = @target.entries if @parent.new_record?
end
def determine_name
@proxy ||= class << self; self; end
@proxy.send(:determine_name, @parent, @options)
end
# The default query used for retrieving the documents from the database.
# In this case we use the common API between Mongoid, ActiveRecord, and
# DataMapper so we can do one-to-many relationships with data in other
# databases.
#
# Example:
#
# association.query
#
# Returns:
#
# A +Criteria+ if a Mongoid association.
# An +Array+ of objects if an ActiveRecord association
# A +Collection+ if a DataMapper association.
def query
@query ||= lambda { @klass.all(:conditions => { @foreign_key => @parent.id }) }
end
# Remove the objects based on conditions.
def remove(method, conditions)
selector = { @foreign_key => @parent.id }.merge(conditions || {})
removed = @klass.send(method, :conditions => selector)
reset; removed
end
# Reset the memoized association on the parent. This will execute the
# database query again.
#
# Example:
#
# association.reset
#
# Returns:
#
# See #query rdoc for return values.
def reset
@parent.send(:reset, @options.name) { query.call }
end
class << self
# Preferred method for creating the new +ReferencesMany+ association.
#
# Options:
#
# document: The +Document+ that contains the relationship.
# options: The association +Options+.
def instantiate(document, options, target = nil)
new(document, options, target)
end
# Returns the macro used to create the association.
def macro
:references_many
end
# Perform an update of the relationship of the parent and child. This
# will assimilate the child +Document+ into the parent's object graph.
#
# Options:
#
# related: The related object
# parent: The parent +Document+ to update.
# options: The association +Options+
#
# Example:
#
# RelatesToOne.update(game, person, options)
def update(target, document, options)
name = determine_name(document, options)
target.each { |child| child.send("#{name}=", document) }
instantiate(document, options, target)
end
protected
def determine_name(document, options)
target = document.class
if (inverse = options.inverse_of) && inverse.is_a?(Array)
inverse = [*inverse].detect { |name| target.respond_to?(name) }
end
if !inverse
association = options.klass.associations.values.detect do |metadata|
metadata.options.klass == target
end
inverse = association.name if association
end
inverse || target.to_s.underscore
end
end
end
end
end