123456789_123456789_123456789_123456789_123456789_

Module: Mongoid::Persistable::Creatable

Relationships & Source Files
Namespace Children
Modules:
Extension / Inclusion / Inheritance Descendants
Included In:
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, ActiveSupport::Concern
Defined in: lib/mongoid/persistable/creatable.rb

Overview

Defines behavior for persistence operations that create new documents.

Instance Method Summary

Instance Method Details

#atomic_inserts ⇒ Hash (private)

Get the atomic insert for embedded documents, either a push or set.

Examples:

Get the inserts.

document.inserts

Returns:

  • (Hash) —

    The insert ops.

[ GitHub ]

  
# File 'lib/mongoid/persistable/creatable.rb', line 38

def atomic_inserts
  { atomic_insert_modifier => { atomic_position => as_attributes } }
end

#conflicting_touch_paths?(operations, touches) ⇒ true | false (private)

Determine whether merging the touch updates into the insert operations would produce an update whose operators target both a path and one of its ancestors or descendants. MongoDB rejects such an update with error 40. This can happen for embeds_many inserts, where the operations target the array (via $push) but the touches include a path inside that array (e.g. items.0.updated_at) when a sibling has a pending touch.

Parameters:

  • operations (Hash) —

    The atomic insert operations.

  • touches (Hash) —

    The touch updates to merge.

Returns:

  • (true | false) —

    Whether any touch path conflicts with a path the insert operations target.

[ GitHub ]

  
# File 'lib/mongoid/persistable/creatable.rb', line 57

def conflicting_touch_paths?(operations, touches)
  targeted = operations.each_value.flat_map do |doc|
    doc.respond_to?(:keys) ? doc.keys.map(&:to_s) : []
  end

  touches.each_key.any? do |touch_path|
    path = touch_path.to_s
    targeted.any? do |target|
      path == target || path.start_with?("#{target}.") ||
        target.start_with?("#{path}.")
    end
  end
end

#insert(options = {}) ⇒ Document

Insert a new document into the database. Will return the document itself whether or not the save was successful.

Examples:

Insert a document.

document.insert

Parameters:

  • options (Hash) (defaults to: {}) —

    Options to pass to insert.

Returns:

  • (Document) —

    The persisted document.

[ GitHub ]

  
# File 'lib/mongoid/persistable/creatable.rb', line 18

def insert(options = {})
  prepare_insert(options) do
    if embedded?
      insert_as_embedded
    else
      insert_as_root
    end
  end
end

#insert_as_embedded ⇒ Document (private)

Insert the embedded document.

When the parent association is touchable (which is the default for embedded_in), the touch updates are merged into the same update_one call that performs the insert. This avoids a second round-trip that the after_save touch callback would otherwise issue. The merge is skipped when a touch path would conflict with a path the insert targets (see #conflicting_touch_paths?) or when rewriting the touch indices to the positional operator would misdirect a touch (see #rewritten_touch_updates); such touches are persisted in their own round-trip right after the insert, so their persistence does not depend on the callback chain running.

Examples:

Insert the document as embedded.

document.insert_as_embedded

Returns:

Raises:

[ GitHub ]

  
# File 'lib/mongoid/persistable/creatable.rb', line 122

def insert_as_embedded
  raise Errors::NoParent.new(self.class.name) unless _parent

  if _parent.new_record?
    _parent.insert
  else
    selector = _parent.atomic_selector
    operations = atomic_inserts

    deferred_touches = merge_touch_updates(selector, operations)

    begin
      _root.collection.find(selector).update_one(
        positionally(selector, operations),
        session: _session
      )
      _root.persist_atomic_operations('$set' => deferred_touches) if deferred_touches
    rescue StandardError
      # If the insert failed, the after_save callback will not run to
      # consume and clear the merged-touch flag, so clear it here.
      Threaded.exit_touch_merged(self)
      raise
    end
  end
end

#insert_as_root ⇒ Document (private)

Insert the root document.

Examples:

Insert the document as root.

document.insert_as_root

Returns:

[ GitHub ]

  
# File 'lib/mongoid/persistable/creatable.rb', line 194

def insert_as_root
  collection.insert_one(as_attributes, session: _session)
end

#merge_touch_updates(selector, operations) ⇒ Hash | nil (private)

Merge the parent chain's pending touch updates into the insert operations when doing so would not produce a conflicting update (see #conflicting_touch_paths? and #rewritten_touch_updates). Either way, marks the touch as merged so the after_save callback does not persist the updates a second time. Returns the touch updates that could not be merged; they are persisted in their own round-trip right after the insert, rather than relying on the after_save callback, so that an aborted callback chain cannot silently drop them.

Parameters:

  • selector (Hash) —

    The update selector.

  • operations (Hash) —

    The atomic insert operations, modified in place when the touches are merged.

Returns:

  • (Hash | nil) —

    The deferred touch updates, if any.

[ GitHub ]

  
# File 'lib/mongoid/persistable/creatable.rb', line 165

def merge_touch_updates(selector, operations)
  return nil unless _touchable_parent?

  field = _association&.inverse_association&.touch_field
  touches = _parent._gather_touch_updates(Time.current, field)
  return nil if touches.blank?

  if conflicting_touch_paths?(operations, touches)
    deferred_touches = touches
  else
    rewritten = rewritten_touch_updates(selector, _parent, field, touches)
    if rewritten
      operations['$set'] = (operations['$set'] || {}).merge(rewritten)
    else
      deferred_touches = touches
    end
  end
  Threaded.begin_touch_merged(self)
  deferred_touches
end

#post_process_insert ⇒ true (private)

Post process an insert, which sets the new record attribute to false and flags all the children as persisted.

Examples:

Post process the insert.

document.post_process_insert

Returns:

  • (true) —

    true.

[ GitHub ]

  
# File 'lib/mongoid/persistable/creatable.rb', line 207

def post_process_insert
  self.new_record = false
  remember_storage_options!
  flag_descendants_persisted
  true
end

#prepare_insert(options = {}) ⇒ Document (private)

Prepare the insert for execution. Validates and runs callbacks, etc.

Examples:

Prepare for insertion.

document.prepare_insert do
  collection.insert(as_document)
end

Parameters:

  • options (Hash) (defaults to: {}) —

    The options.

Returns:

Raises:

[ GitHub ]

  
# File 'lib/mongoid/persistable/creatable.rb', line 226

def prepare_insert(options = {})
  raise Errors::ReadonlyDocument.new(self.class) if readonly? && !Mongoid.legacy_readonly
  return self if performing_validations?(options) &&
                 invalid?(options[:context] || :create)

  ensure_client_compatibility!
  run_callbacks(:commit, with_children: true, skip_if: -> { in_transaction? }) do
    run_callbacks(:save, with_children: false) do
      run_callbacks(:create, with_children: false) do
        run_callbacks(:persist_parent, with_children: false) do
          _mongoid_run_child_callbacks(:save) do
            _mongoid_run_child_callbacks(:create) do
              result = yield(self)
              if !result.is_a?(Document) || result.errors.empty?
                post_process_insert
                post_process_persist(result, options)
              end
            end
          end
        end
      end
    end
  end
  self
end

#rewritten_touch_updates(selector, parent, field, touches) ⇒ Hash | nil (private)

Rewrite the indices in the touch updates to the positional operator, as the final update will do to the merged operations. The touches of the parent chain target the inserted element, so rewriting them is safe, and merging the rewritten keys into the operations lets the final update reuse them as-is. A pending touch on a different element of the same array, however, would be rewritten to the same positional path and silently touch the wrong document; in that case this method returns nil and the caller persists the touch updates in their own round-trip after the insert instead of merging them.

Parameters:

  • selector (Hash) —

    The update selector.

  • parent (Document) —

    The parent document being touched.

  • field (Symbol, String, nil) —

    The parent's custom touch field.

  • touches (Hash) —

    The touch updates to rewrite.

Returns:

  • (Hash | nil) —

    The rewritten touch updates, or nil if any touch would be misdirected by the rewriting.

[ GitHub ]

  
# File 'lib/mongoid/persistable/creatable.rb', line 90

def rewritten_touch_updates(selector, parent, field, touches)
  rewritten = positionally(selector, { '$set' => touches })['$set']

  chain_paths = parent._touchable_chain_paths(field)
  touches.each_key do |key|
    path = key.to_s
    next if rewritten.key?(path)

    return nil unless chain_paths.include?(path)
  end
  rewritten
end