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
-
#insert(options = {}) ⇒ Document
Insert a new document into the database.
-
#atomic_inserts ⇒ Hash
private
Internal use only
Get the atomic insert for embedded documents, either a push or set.
-
#conflicting_touch_paths?(operations, touches) ⇒ true | false
private
Internal use only
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.
-
#insert_as_embedded ⇒ Document
private
Internal use only
Insert the embedded document.
-
#insert_as_root ⇒ Document
private
Internal use only
Insert the root document.
-
#merge_touch_updates(selector, operations) ⇒ Hash | nil
private
Internal use only
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).
-
#post_process_insert ⇒ true
private
Internal use only
Post process an insert, which sets the new record attribute to false and flags all the children as persisted.
-
#prepare_insert(options = {}) ⇒ Document
private
Internal use only
Prepare the insert for execution.
-
#rewritten_touch_updates(selector, parent, field, touches) ⇒ Hash | nil
private
Internal use only
Rewrite the indices in the touch updates to the positional operator, as the final update will do to the merged operations.
Instance Method Details
#atomic_inserts ⇒ Hash (private)
Get the atomic insert for embedded documents, either a push or set.
# 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.
# 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.
# File 'lib/mongoid/persistable/creatable.rb', line 18
def insert( = {}) prepare_insert() do if 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.
# File 'lib/mongoid/persistable/creatable.rb', line 122
def 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.
# 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.
# 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.
# File 'lib/mongoid/persistable/creatable.rb', line 207
def post_process_insert self.new_record = false flag_descendants_persisted true end
#prepare_insert(options = {}) ⇒ Document (private)
Prepare the insert for execution. Validates and runs callbacks, etc.
# File 'lib/mongoid/persistable/creatable.rb', line 226
def prepare_insert( = {}) raise Errors::ReadonlyDocument.new(self.class) if readonly? && !Mongoid.legacy_readonly return self if performing_validations?() && invalid?([: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, ) 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.
# 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