class Mongoid::Association::Referenced::HasAndBelongsToMany::Proxy

Transparent proxy for has_and_belongs_to_many associations. An instance of this class is returned when calling the association getter method on the subject document. This class inherits from Mongoid::Association::Proxy and forwards most of its methods to the target of the association, i.e. the array of documents on the opposite-side collection which must be loaded.

Public Instance Methods

<<(*args) click to toggle source

Appends a document or array of documents to the association. Will set the parent and update the index in the process.

@example Append a document.

person.posts << post

@example Push a document.

person.posts.push(post)

@example Concat with other documents.

person.posts.concat([ post_one, post_two ])

@param [ Document… ] *args Any number of documents.

@return [ Array<Document> ] The loaded docs.

rubocop:disable Metrics/AbcSize

# File lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb, line 58
def <<(*args)
  docs = args.flatten
  return concat(docs) if docs.size > 1

  if (doc = docs.first)
    append(doc) do
      # We ignore the changes to the value for the foreign key in the
      # changed_attributes hash in this block of code for two reasons:
      #
      # 1) The add_to_set method deletes the value for the foreign
      #    key in the changed_attributes hash, but if we enter this
      #    method with a value for the foreign key in the
      #    changed_attributes hash, then we want it to exist outside
      #    this method as well. It's used later on in the Syncable
      #    module to set the inverse foreign keys.
      # 2) The reset_unloaded method accesses the value for the foreign
      #    key on _base, which causes it to get added to the
      #    changed_attributes hash. This happens because when reading
      #    a "resizable" attribute, it is automatically added to the
      #    changed_attributes hash. This is true only for the foreign
      #    key value for HABTM associations as the other associations
      #    use strings for their foreign key values. For consistency
      #    with the other associations, we ignore this addition to
      #    the changed_attributes hash.
      #    See MONGOID-4843 for a longer discussion about this.
      reset_foreign_key_changes do
        _base.add_to_set(foreign_key => doc.public_send(_association.primary_key))
        doc.save if child_persistable?(doc)
        reset_unloaded
      end
    end
  end
  unsynced(_base, foreign_key) and self
end
Also aliased as: push
build(attributes = {}, type = nil) { |doc| ... } click to toggle source

Build a new document from the attributes and append it to this association without saving.

@example Build a new document on the association.

person.posts.build(:title => "A new post")

@param [ Hash ] attributes The attributes of the new document. @param [ Class ] type The optional subclass to build.

@return [ Document ] The new document.

# File lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb, line 123
def build(attributes = {}, type = nil)
  doc = Factory.execute_build(type || klass, attributes, execute_callbacks: false)
  append(doc)
  doc.apply_post_processed_defaults
  _base.public_send(foreign_key).push(doc.public_send(_association.primary_key))
  unsynced(doc, inverse_foreign_key)
  yield(doc) if block_given?
  doc.run_pending_callbacks
  doc
end
Also aliased as: new
clear(replacement = [])
Alias for: nullify
concat(documents) click to toggle source

Appends an array of documents to the association. 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.

# File lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb, line 105
def concat(documents)
  ids, docs, inserts = {}, [], []
  documents.each { |doc| append_document(doc, ids, docs, inserts) }
  _base.push(foreign_key => ids.keys) if persistable? || _creating?
  persist_delayed(docs, inserts)
  self
end
delete(document) click to toggle source

Delete the document from the association. This will set the foreign key on the document to nil. If the dependent options on the association are :delete_all or :destroy the appropriate removal will occur.

@example Delete the document.

person.posts.delete(post)

@param [ Document ] document The document to remove.

@return [ Document ] The matching document.

# File lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb, line 146
def delete(document)
  doc = super
  if doc && persistable?
    _base.pull(foreign_key => doc.public_send(_association.primary_key))
    _target._unloaded = criteria
    unsynced(_base, foreign_key)
  end
  doc
end
Also aliased as: delete_one
delete_one(document)

Mongoid::Extensions::Array defines Array#delete_one, so we need to make sure that method behaves reasonably on proxies, too.

Alias for: delete
new(attributes = {}, type = nil)
Alias for: build
nullify(replacement = []) click to toggle source

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.

@example Nullify the association.

person.preferences.nullify

@param [ Array<Document> ] replacement The replacement documents.

# File lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb, line 168
def nullify(replacement = [])
  _target.each { |doc| execute_callback :before_remove, doc }
  cleanup_inverse_for(replacement) unless _association.forced_nil_inverse?
  _base.set(foreign_key => _base.public_send(foreign_key).clear) if persistable?
  clear_target_for_nullify
end
Also aliased as: nullify_all, clear, purge
nullify_all(replacement = [])
Alias for: nullify
purge(replacement = [])
Alias for: nullify
push(*args)

rubocop:enable Metrics/AbcSize

Alias for: <<
substitute(replacement) click to toggle source

Substitutes the supplied target documents for the existing documents in the association. If the new target is nil, perform the necessary deletion.

@example Replace the association. person.preferences.substitute([ new_post ])

@param [ Array<Document> ] replacement The replacement target.

@return [ Many ] The association.

# File lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb, line 189
def substitute(replacement)
  purge(replacement)
  if replacement.blank?
    reset_unloaded
    clear_foreign_key_changes
  else
    push(replacement.compact.uniq)
  end
  self
end
unscoped() click to toggle source

Get a criteria for the documents without the default scoping applied.

@example Get the unscoped criteria.

person.preferences.unscoped

@return [ Criteria ] The unscoped criteria.

# File lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb, line 207
def unscoped
  klass.unscoped.any_in(_id: _base.public_send(foreign_key))
end

Private Instance Methods

append(document) { || ... } click to toggle source

Appends the document to the target array, updating the index on the document at the same time.

@example Append the document to the association.

relation.append(document)

@param [ Document ] document The document to append to the target.

# File lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb, line 243
def append(document)
  execute_callbacks_around(:add, document) do
    _target.push(document)
    characterize_one(document)
    bind_one(document)
    yield if block_given?
  end
end
append_document(doc, ids, docs, inserts) click to toggle source

Processes a single document as part of a “concat“ command.

@param [ Mongoid::Document ] doc the document to append @param [ Hash ] ids the mapping of primary keys that have been

visited

@param [ Array ] docs the list of new docs to be inserted later,

in bulk

@param [ Array ] inserts the list of Hashes of attributes that will

be inserted (corresponding to the ``docs`` list)

rubocop:disable Metrics/AbcSize

# File lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb, line 365
def append_document(doc, ids, docs, inserts)
  return unless doc

  append(doc)

  pk = doc.public_send(_association.primary_key)
  if persistable? || _creating?
    ids[pk] = true
    save_or_delay(doc, docs, inserts)
  else
    existing = _base.public_send(foreign_key)
    return if existing.include?(pk)

    existing.push(pk)
    unsynced(_base, foreign_key)
  end
end
binding() click to toggle source

Instantiate the binding associated with this association.

@example Get the binding.

relation.binding([ address ])

@return [ Binding ] The binding.

# File lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb, line 258
def binding
  HasAndBelongsToMany::Binding.new(_base, _target, _association)
end
child_persistable?(doc) click to toggle source

Determine if the child document should be persisted.

@api private

@example Is the child persistable?

relation.child_persistable?(doc)

@param [ Document ] doc The document.

@return [ true | false ] If the document can be persisted.

# File lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb, line 272
def child_persistable?(doc)
  (persistable? || _creating?) &&
    !(doc.persisted? && _association.forced_nil_inverse?)
end
cleanup_inverse_for(replacement) click to toggle source

Does the cleanup for the inverse of the association when replacing the relation with another list of documents.

@param [ Array<Document> | nil ] replacement the list of documents

that will replace the current list.
# File lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb, line 309
def cleanup_inverse_for(replacement)
  if replacement
    new_ids = replacement.collect { |doc| doc.public_send(_association.primary_key) }
    objects_to_clear = _base.public_send(foreign_key) - new_ids
    criteria(objects_to_clear).pull(inverse_foreign_key => inverse_primary_key)
  else
    criteria.pull(inverse_foreign_key => inverse_primary_key)
  end
end
clear_foreign_key_changes() click to toggle source

Clears the foreign key from the changed_attributes hash.

This is, in general, used to clear the foreign key from the changed_attributes hash for consistency with the other referenced associations.

@api private

# File lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb, line 220
def clear_foreign_key_changes
  _base.changed_attributes.delete(foreign_key)
end
clear_target_for_nullify() click to toggle source

Clears the _target list and executes callbacks for each document. If an exception occurs in an after_remove hook, the exception is saved, the processing completes, and then the exception is re-raised.

@return [ Array<Document> ] the replacement documents

# File lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb, line 336
def clear_target_for_nullify
  after_remove_error = nil
  many_to_many = _target.clear do |doc|
    unbind_one(doc)
    doc.changed_attributes.delete(inverse_foreign_key) unless _association.forced_nil_inverse?

    begin
      execute_callback :after_remove, doc
    rescue StandardError => e
      after_remove_error = e
    end
  end

  raise after_remove_error if after_remove_error

  many_to_many
end
criteria(id_list = nil) click to toggle source

Returns the criteria object for the target class with its documents set to target.

@example Get a criteria for the association.

relation.criteria

@return [ Criteria ] A new criteria.

# File lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb, line 284
def criteria(id_list = nil)
  _association.criteria(_base, id_list)
end
inverse_primary_key() click to toggle source

The inverse primary key

@return [ Object ] the inverse primary key

# File lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb, line 322
def inverse_primary_key
  if (field = _association.options[:inverse_primary_key])
    _base.public_send(field)
  else
    _base._id
  end
end
reset_foreign_key_changes() { || ... } click to toggle source

Reset the value in the changed_attributes hash for the foreign key to its value before executing the given block.

@api private

# File lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb, line 228
def reset_foreign_key_changes
  prior_fk_change = _base.changed_attributes.key?(foreign_key)
  fk = _base.changed_attributes[foreign_key].dup
  yield if block_given?
  _base.changed_attributes[foreign_key] = fk
  clear_foreign_key_changes unless prior_fk_change
end
unsynced(doc, key) click to toggle source

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.

# File lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb, line 299
def unsynced(doc, key)
  doc._synced[key] = false
  true
end