module Mongoid::Association::Accessors

This module contains all the behavior related to accessing associations through the getters and setters, and how to delegate to builders to create new ones.

Private Class Methods

define_builder!(association) click to toggle source

Defines a builder method for an embeds_one association. This is defined as build_name.

@example

Person.define_builder!(association)

@param [ Association ] association The association for the association.

@return [ Class ] The class being set up.

@since 2.0.0.rc.1

# File lib/mongoid/association/accessors.rb, line 385
def self.define_builder!(association)
  name = association.name
  association.inverse_class.tap do |klass|
    klass.re_define_method("build_#{name}") do |*args|
      attributes, _options = parse_args(*args)
      document = Factory.build(association.relation_class, attributes)
      _building do
        child = send("#{name}=", document)
        child.run_callbacks(:build)
        child
      end
    end
  end
end
define_creator!(association) click to toggle source

Defines a creator method for an embeds_one association. This is defined as create_name. After the object is built it will immediately save.

@example

Person.define_creator!(association)

@param [ Association ] association The association for the association.

@return [ Class ] The class being set up.

@since 2.0.0.rc.1

# File lib/mongoid/association/accessors.rb, line 412
def self.define_creator!(association)
  name = association.name
  association.inverse_class.tap do |klass|
    klass.re_define_method("create_#{name}") do |*args|
      attributes, _options = parse_args(*args)
      document = Factory.build(association.klass, attributes)
      doc = _assigning do
        send("#{name}=", document)
      end
      doc.save
      save if new_record? && association.stores_foreign_key?
      doc
    end
  end
end
define_existence_check!(association) click to toggle source

Adds the existence check for associations.

@example Add the existence check.

Person.define_existence_check!(association)

@example Check if an association exists.

person = Person.new
person.has_game?
person.game?

@param [ Association ] association The association.

@return [ Class ] The model being set up.

@since 3.0.0

# File lib/mongoid/association/accessors.rb, line 265
      def self.define_existence_check!(association)
        name = association.name
        association.inverse_class.tap do |klass|
          klass.module_eval <<-END, __FILE__, __LINE__ + 1
              def #{name}?
                without_autobuild { !__send__(:#{name}).blank? }
              end
              alias :has_#{name}? :#{name}?
          END
        end
      end
define_getter!(association) click to toggle source

Defines the getter for the association. Nothing too special here: just return the instance variable for the association if it exists or build the thing.

@example Set up the getter for the association.

Person.define_getter!(association)

@param [ Association ] association The association metadata for the association.

@return [ Class ] The class being set up.

@since 2.0.0.rc.1

# File lib/mongoid/association/accessors.rb, line 289
def self.define_getter!(association)
  name = association.name
  association.inverse_class.tap do |klass|
    klass.re_define_method(name) do |reload = false|
      value = get_relation(name, association, nil, reload)
      if value.nil? && association.autobuilding? && !without_autobuild?
        value = send("build_#{name}")
      end
      value
    end
  end
end
define_ids_getter!(association) click to toggle source

Defines the getter for the ids of documents in the association. Should be specify only for referenced many associations.

@example Set up the ids getter for the association.

Person.define_ids_getter!(association)

@param [ Association ] association The association metadata for the association.

@return [ Class ] The class being set up.

# File lib/mongoid/association/accessors.rb, line 311
def self.define_ids_getter!(association)
  ids_method = "#{association.name.to_s.singularize}_ids"
  association.inverse_class.tap do |klass|
    klass.re_define_method(ids_method) do
      send(association.name).only(:_id).map(&:_id)
    end
  end
end
define_ids_setter!(association) click to toggle source

Defines the setter method that allows you to set documents in this association by their ids. The defined setter, finds documents with given ids and invokes regular association setter with found documents. Ids setters should be defined only for referenced many associations.

@example Set up the id_setter for the association.

 Person.define_ids_setter!(association)

@param [ Association ] association The association for the association.

@return [ Class ] The class being set up.
# File lib/mongoid/association/accessors.rb, line 365
def self.define_ids_setter!(association)
  ids_method = "#{association.name.to_s.singularize}_ids="
  association.inverse_class.tap do |klass|
    klass.re_define_method(ids_method) do |ids|
      send(association.setter, association.relation_class.find(ids.reject(&:blank?)))
    end
  end
end
define_setter!(association) click to toggle source

Defines the setter for the association. This does a few things based on some conditions. If there is an existing association, a target substitution will take place, otherwise a new association will be created with the supplied target.

@example Set up the setter for the association.

Person.define_setter!(association)

@param [ Association ] association The association metadata for the association.

@return [ Class ] The class being set up.

@since 2.0.0.rc.1

# File lib/mongoid/association/accessors.rb, line 333
def self.define_setter!(association)
  name = association.name
  association.inverse_class.tap do |klass|
    klass.re_define_method("#{name}=") do |object|
      without_autobuild do
        if value = get_relation(name, association, object)
          if value.respond_to?(:substitute)
            set_relation(name, value.substitute(object.substitutable))
          else
            value = __build__(name, value, association)
            set_relation(name, value.substitute(object.substitutable))
          end
        else
          __build__(name, object.substitutable, association)
        end
      end
    end
  end
end

Public Instance Methods

__build__(name, object, association, selected_fields = nil) click to toggle source

Builds the related document and creates the association unless the document is nil, then sets the association on this document.

@example Build the association.

person.__build__(:addresses, { :_id => 1 }, association)

@param [ String, Symbol ] name The name of the association. @param [ Hash, BSON::ObjectId ] object The id or attributes to use. @param [ Association ] association The association metadata. @param [ Hash ] selected_fields Fields which were retrieved via only.

If selected_fields is specified, fields not listed in it will not be
accessible in the built document.

@return [ Proxy ] The association.

@since 2.0.0.rc.1

# File lib/mongoid/association/accessors.rb, line 29
def __build__(name, object, association, selected_fields = nil)
  relation = create_relation(object, association, selected_fields)
  set_relation(name, relation)
end
create_relation(object, association, selected_fields = nil) click to toggle source

Create an association from an object and association metadata.

@example Create the association.

person.create_relation(document, association)

@param [ Document, Array<Document> ] object The association target. @param [ Association ] association The association metadata. @param [ Hash ] selected_fields Fields which were retrieved via only.

If selected_fields is specified, fields not listed in it will not be
accessible in the created association document.

@return [ Proxy ] The association.

@since 2.0.0.rc.1

# File lib/mongoid/association/accessors.rb, line 48
def create_relation(object, association, selected_fields = nil)
  type = @attributes[association.inverse_type]
  target = association.build(self, object, type, selected_fields)
  target ? association.create_relation(self, target) : nil
end
reset_relation_criteria(name) click to toggle source

Resets the criteria inside the association proxy. Used by many-to-many associations to keep the underlying ids array in sync.

@example Reset the association criteria.

person.reset_relation_criteria(:preferences)

@param [ Symbol ] name The name of the association.

@since 3.0.14

# File lib/mongoid/association/accessors.rb, line 63
def reset_relation_criteria(name)
  if instance_variable_defined?("@_#{name}")
    send(name).reset_unloaded
  end
end
set_relation(name, relation) click to toggle source

Set the supplied association to an instance variable on the class with the provided name. Used as a helper just for code cleanliness.

@example Set the proxy on the document.

person.set(:addresses, addresses)

@param [ String, Symbol ] name The name of the association. @param [ Proxy ] relation The association to set.

@return [ Proxy ] The association.

@since 2.0.0.rc.1

# File lib/mongoid/association/accessors.rb, line 81
def set_relation(name, relation)
  instance_variable_set("@_#{name}", relation)
end

Private Instance Methods

_mongoid_filter_selected_fields(assoc_key) click to toggle source

Returns a subset of __selected_fields attribute applicable to the (embedded) association with the given key, or nil if no projection is to be performed.

Also returns nil if exclusionary projection was requested but it does not exclude the field of the association.

For example, if __selected_fields is {‘a’ => 1, ‘b.c’ => 2, ‘b.c.f’ => 3}, and assoc_key is ‘b’, return value would be {‘c’ => 2, ‘c.f’ => 3}.

@param [ String ] assoc_key

@return [ Hash | nil ]

@api private

# File lib/mongoid/association/accessors.rb, line 135
def _mongoid_filter_selected_fields(assoc_key)
  return nil unless __selected_fields

  # If the list of fields was specified using #without instead of #only
  # and the provided list does not include the association, any of its
  # fields should be allowed.
  if __selected_fields.values.all? { |v| v == 0 } &&
    __selected_fields.keys.none? { |k| k.split('.', 2).first == assoc_key }
  then
    return nil
  end

  projecting_assoc = false

  filtered = {}
  __selected_fields.each do |k, v|
    bits = k.split('.')

    # If we are asked to project an association, we need all of that
    # association's fields. However, we may be asked to project
    # an association *and* its fields in the same query. In this case
    # behavior differs according to server version:
    #
    # 4.2 and lower take the most recent projection specification, meaning
    # projecting foo followed by foo.bar effectively projects foo.bar and
    # projecting foo.bar followed by foo effectively projects foo.
    # To match this behavior we need to track when we are being asked
    # to project the association and when we are asked to project a field,
    # and if we are asked to project the association last we need to
    # remove any field projections.
    #
    # 4.4 (and presumably higher) do not allow projection to be on an
    # association and its field, so it doesn't matter what we do. Hence
    # we just need to handle the 4.2 and lower case correctly.
    if bits.first == assoc_key
      # Projecting the entire association OR some of its fields
      if bits.length > 1
        # Projecting a field
        bits.shift
        filtered[bits.join('.')] = v
        projecting_assoc = false
      else
        # Projecting the entire association
        projecting_assoc = true
      end
    end
  end

  if projecting_assoc
    # The last projection was of the entire association; we may have
    # also been projecting fields, but discard the field projections
    # and return nil indicating we want the entire association.
    return nil
  end

  # Positional projection is specified as "foo.$". In this case the
  # document that the $ is referring to should be retrieved with all
  # fields. See https://docs.mongodb.com/manual/reference/operator/projection/positional/
  # and https://jira.mongodb.org/browse/MONGOID-4769.
  if filtered.keys == %w($)
    filtered = nil
  end

  filtered
end
get_relation(name, association, object, reload = false) click to toggle source

Get the association. Extracted out from the getter method to avoid infinite recursion when overriding the getter.

@api private

@example Get the association.

document.get_relation(:name, association)

@param [ Symbol ] name The name of the association. @param [ Association ] association The association metadata. @param [ Object ] object The object used to build the association. @param [ true, false ] reload If the association is to be reloaded.

@return [ Proxy ] The association.

@since 3.0.16

# File lib/mongoid/association/accessors.rb, line 103
def get_relation(name, association, object, reload = false)
  if !reload && (value = ivar(name)) != false
    value
  else
    _building do
      _loading do
        if object && needs_no_database_query?(object, association)
          __build__(name, object, association)
        else
          selected_fields = _mongoid_filter_selected_fields(association.key)
          __build__(name, attributes[association.key], association, selected_fields)
        end
      end
    end
  end
end
needs_no_database_query?(object, association) click to toggle source
# File lib/mongoid/association/accessors.rb, line 201
def needs_no_database_query?(object, association)
  object.is_a?(Document) && !object.embedded? &&
      object._id == attributes[association.key]
end
parse_args(*args) click to toggle source

Parse out the attributes and the options from the args passed to a build_ or create_ methods.

@example Parse the args.

doc.parse_args(:name => "Joe")

@param [ Array ] args The arguments.

@return [ Array<Hash> ] The attributes and options.

@since 2.3.4

# File lib/mongoid/association/accessors.rb, line 246
def parse_args(*args)
  [args.first || {}, args.size > 1 ? args[1] : {}]
end
without_autobuild() { || ... } click to toggle source

Yield to the block with autobuild functionality turned off.

@example Execute without autobuild.

document.without_autobuild do
  document.name
end

@return [ Object ] The result of the yield.

@since 3.0.0

# File lib/mongoid/association/accessors.rb, line 228
def without_autobuild
  Threaded.begin_execution("without_autobuild")
  yield
ensure
  Threaded.exit_execution("without_autobuild")
end
without_autobuild?() click to toggle source

Is the current code executing without autobuild functionality?

@example Is autobuild disabled?

document.without_autobuild?

@return [ true, false ] If autobuild is disabled.

@since 3.0.0

# File lib/mongoid/association/accessors.rb, line 214
def without_autobuild?
  Threaded.executing?(:without_autobuild)
end