# encoding: utf-8
require "mongoid/associations/proxy"
require "mongoid/associations/belongs_to_related"
require "mongoid/associations/embedded_in"
require "mongoid/associations/embeds_many"
require "mongoid/associations/embeds_one"
require "mongoid/associations/has_many_related"
require "mongoid/associations/has_one_related"
require "mongoid/associations/options"
require "mongoid/associations/meta_data"
module Mongoid # :nodoc:
module Associations #:nodoc:
extend ActiveSupport::Concern
included do
cattr_accessor :embedded
self.embedded = false
class_inheritable_accessor :associations
self.associations = {}
delegate :embedded, :embedded?, :to => "self.class"
end
module InstanceMethods
# Returns the associations for the +Document+.
def associations
self.class.associations
end
# are we in an embeds_many?
def embedded_many?
embedded? and _parent.associations[association_name].association == EmbedsMany
end
# Update all the dirty child documents after an update.
def update_embedded(name)
association = send(name)
association.to_a.each { |doc| doc.save if doc.changed? || doc.new_record? } unless association.blank?
end
# Update the one-to-one relational association for the name.
def update_association(name)
association = send(name)
association.save if new_record? && !association.nil?
end
# Updates all the one-to-many relational associations for the name.
def update_associations(name)
send(name).each { |doc| doc.save } if new_record?
end
end
module ClassMethods
# Adds a relational association from the child Document to a Document in
# another database or collection.
#
# Options:
#
# name: A +Symbol+ that is the related class name.
#
# Example:
#
# class Game
# include Mongoid::Document
# belongs_to_related :person
# end
#
def belongs_to_related(name, options = {}, &block)
opts = optionize(name, options, fk(name, options), &block)
associate(Associations::BelongsToRelated, opts)
field(opts.foreign_key, :type => Mongoid.use_object_ids ? BSON::ObjectId : String)
index(opts.foreign_key) unless embedded?
end
# Gets whether or not the document is embedded.
#
# Example:
#
# Person.embedded?
#
# Returns:
#
# true if embedded, false if not.
def embedded?
!!self.embedded
end
# Adds the association back to the parent document. This macro is
# necessary to set the references from the child back to the parent
# document. If a child does not define this association calling
# persistence methods on the child object will cause a save to fail.
#
# Options:
#
# name: A +Symbol+ that matches the name of the parent class.
#
# Example:
#
# class Person
# include Mongoid::Document
# embeds_many :addresses
# end
#
# class Address
# include Mongoid::Document
# embedded_in :person, :inverse_of => :addresses
# end
def embedded_in(name, options = {}, &block)
unless options.has_key?(:inverse_of)
raise Errors::InvalidOptions.new("Options for embedded_in association must include :inverse_of")
end
self.embedded = true
associate(Associations::EmbeddedIn, optionize(name, options, nil, &block))
end
alias :belongs_to :embedded_in
# Adds the association from a parent document to its children. The name
# of the association needs to be a pluralized form of the child class
# name.
#
# Options:
#
# name: A +Symbol+ that is the plural child class name.
#
# Example:
#
# class Person
# include Mongoid::Document
# embeds_many :addresses
# end
#
# class Address
# include Mongoid::Document
# embedded_in :person, :inverse_of => :addresses
# end
def embeds_many(name, options = {}, &block)
associate(Associations::EmbedsMany, optionize(name, options, nil, &block))
unless name == :versions
after_update do |document|
document.update_embedded(name)
end
end
end
alias :embed_many :embeds_many
alias :has_many :embeds_many
# Adds the association from a parent document to its child. The name
# of the association needs to be a singular form of the child class
# name.
#
# Options:
#
# name: A +Symbol+ that is the plural child class name.
#
# Example:
#
# class Person
# include Mongoid::Document
# embeds_one :name
# end
#
# class Name
# include Mongoid::Document
# embedded_in :person
# end
def embeds_one(name, options = {}, &block)
opts = optionize(name, options, nil, &block)
type = Associations::EmbedsOne
associate(type, opts)
add_builder(type, opts)
add_creator(type, opts)
after_update do |document|
document.update_embedded(name)
end
end
alias :embed_one :embeds_one
alias :has_one :embeds_one
# Adds a relational association from the Document to many Documents in
# another database or collection.
#
# Options:
#
# name: A +Symbol+ that is the related class name pluralized.
#
# Example:
#
# class Person
# include Mongoid::Document
# has_many_related :posts
# end
#
def has_many_related(name, options = {}, &block)
associate(Associations::HasManyRelated, optionize(name, options, fk(self.name, options), &block))
before_save do |document|
document.update_associations(name)
end
end
# Adds a relational association from the Document to one Document in
# another database or collection.
#
# Options:
#
# name: A +Symbol+ that is the related class name pluralized.
#
# Example:
#
# class Person
# include Mongoid::Document
# has_one_related :game
# end
def has_one_related(name, options = {}, &block)
associate(Associations::HasOneRelated, optionize(name, options, fk(self.name, options), &block))
before_save do |document|
document.update_association(name)
end
end
# Returns the macro associated with the supplied association name. This
# will return has_one, has_many, belongs_to or nil.
#
# Options:
#
# name: The association name.
#
# Example:
#
# Person.reflect_on_association(:addresses)
def reflect_on_association(name)
association = associations[name.to_s]
association ? association.macro : nil
end
protected
# Adds the association to the associations hash with the type as the key,
# then adds the accessors for the association. The defined setters and
# getters for the associations will perform the necessary memoization.
#
# Example:
#
# Person.associate(EmbedsMany, { :name => :addresses })
def associate(type, options)
name = options.name.to_s
associations[name] = MetaData.new(type, options)
define_method(name) { memoized(name) { type.instantiate(self, options) } }
define_method("#{name}=") do |object|
unmemoize(name)
memoized(name) { type.update(object, self, options) }
end
end
# Adds a builder for a has_one association. This comes in the form of
# build_name(attributes)
def add_builder(type, options)
name = options.name.to_s
define_method("build_#{name}") do |attrs|
reset(name) { type.new(self, (attrs || {}).stringify_keys, options) }
end
end
# Adds a creator for a has_one association. This comes in the form of
# create_name(attributes)
def add_creator(type, options)
name = options.name.to_s
define_method("create_#{name}") do |attrs|
document = send("build_#{name}", attrs)
document.save; document
end
end
# build the options given the params.
def optionize(name, options, foreign_key, &block)
Associations::Options.new(
options.merge(:name => name, :foreign_key => foreign_key, :extend => block)
)
end
# Find the foreign key.
def fk(name, options)
options[:foreign_key] || name.to_s.foreign_key
end
# Build the association options.
def build_options(name, options, &block)
Associations::Options.new(
options.merge(
:name => name,
:foreign_key => foreign_key(name, options),
:extend => block
)
)
end
end
end
end