Module: Mongoid::Association::EagerLoadable
| Relationships & Source Files | |
| Extension / Inclusion / Inheritance Descendants | |
|
Included In:
| |
| Defined in: | lib/mongoid/association/eager_loadable.rb |
Overview
This module defines the eager loading behavior for criteria.
Instance Attribute Summary
-
#eager_loadable? ⇒ true | false
readonly
Indicates whether the criteria has association inclusions which should be eager loaded.
Instance Method Summary
- #create_pipeline(current_assoc, mapping)
-
#eager_load(docs) ⇒ Array<Mongoid::Document>
Load the associations for the given documents.
-
#eager_load_with_lookup ⇒ Array<Mongoid::Document>
Load the associations for the given documents using $lookup.
-
#preload(associations, docs)
Load the associations for the given documents.
-
#preload_for_lookup(criteria)
Load the associations for the given documents.
- #switch_local_and_foreign_fields?(association) ⇒ Boolean
-
#cross_cluster_inclusions ⇒ Array<Mongoid::Association::Relatable>
private
Returns the inclusions whose target class resides in a different cluster than the root class.
-
#docs_for_lookup_fallback ⇒ Array<Mongoid::Document>
private
Returns the materialized documents to use when falling back from $lookup to #includes-style preloading.
Instance Attribute Details
#eager_loadable? ⇒ true | false (readonly)
Indicates whether the criteria has association inclusions which should be eager loaded.
# File 'lib/mongoid/association/eager_loadable.rb', line 13
def eager_loadable? !criteria.inclusions.empty? end
Instance Method Details
#create_pipeline(current_assoc, mapping)
[ GitHub ]# File 'lib/mongoid/association/eager_loadable.rb', line 147
def create_pipeline(current_assoc, mapping) # Build nested pipeline for children and ordering pipeline_stages = [] # For belongs_to and has_and_belongs_to_many, the foreign key is on the current document # For has_many/has_one, the foreign key is on the related document if switch_local_and_foreign_fields?(current_assoc) local_field = current_assoc.foreign_key foreign_field = current_assoc.primary_key else local_field = current_assoc.primary_key foreign_field = current_assoc.foreign_key end # Build the 'as' field with embedded path prefix if needed as_field = current_assoc.name.to_s stage = { '$lookup' => { 'from' => current_assoc.klass.collection.name, 'localField' => local_field, 'foreignField' => foreign_field, 'as' => as_field } } # Add ordering if defined on the association, or default to _id for consistent order if current_assoc.order sort_spec = current_assoc.order.is_a?(Hash) ? current_assoc.order : { current_assoc.order => 1 } pipeline_stages << { '$sort' => sort_spec } else # Default to sorting by _id to maintain insertion order consistency pipeline_stages << { '$sort' => { '_id' => 1 } } end # Add nested lookups for child associations # Child associations don't need the embedded_path prefix since they're referenced from the looked-up document # Remove this class from the mapping to prevent infinite loops with circular references class_name = current_assoc.klass.to_s if child_assocs = mapping.delete(class_name) child_assocs.each do |child| pipeline_stages << create_pipeline(child, mapping) end end # Always add pipeline since we always have at least $sort stage['$lookup']['pipeline'] = pipeline_stages stage end
#cross_cluster_inclusions ⇒ Array<Mongoid::Association::Relatable> (private)
Returns the inclusions whose target class resides in a different cluster than the root class.
# File 'lib/mongoid/association/eager_loadable.rb', line 135
def cross_cluster_inclusions root_client = klass.client_name criteria.inclusions.reject { |assoc| assoc.klass.client_name == root_client } end
#docs_for_lookup_fallback ⇒ Array<Mongoid::Document> (private)
Returns the materialized documents to use when falling back from $lookup to #includes-style preloading. Must be implemented by each concrete context class.
# File 'lib/mongoid/association/eager_loadable.rb', line 127
def docs_for_lookup_fallback raise NotImplementedError, "#{self.class} must implement #docs_for_lookup_fallback" end
#eager_load(docs) ⇒ Array<Mongoid::Document>
Load the associations for the given documents.
# File 'lib/mongoid/association/eager_loadable.rb', line 22
def eager_load(docs) docs.tap do |d| preload(criteria.inclusions, d) if eager_loadable? end end
#eager_load_with_lookup ⇒ Array<Mongoid::Document>
Load the associations for the given documents using $lookup.
If any of the associated collections reside in a different cluster than the root class, falls back to the #includes behavior and logs a warning.
# File 'lib/mongoid/association/eager_loadable.rb', line 34
def eager_load_with_lookup offenders = cross_cluster_inclusions if offenders.any? root_client = klass.client_name offender_list = offenders.map { |a| "#{a.name} (#{a.klass.client_name})" }.join(', ') Mongoid.logger.warn( 'eager_load cannot use $lookup aggregation because the following associations ' \ "reside in a different cluster than #{klass} (client: #{root_client}): " \ "#{offender_list}. Falling back to #includes behavior." ) return eager_load(docs_for_lookup_fallback) end preload_for_lookup(criteria) end
#preload(associations, docs)
Load the associations for the given documents. This will be done recursively to load the associations of the given documents’ associated documents.
# File 'lib/mongoid/association/eager_loadable.rb', line 57
def preload(associations, docs) assoc_map = associations.group_by(&:inverse_class_name) docs_map = {} queue = [ klass.to_s ] # account for single-collection inheritance queue.push(klass.root_class.to_s) if klass != klass.root_class while klass = queue.shift next unless as = assoc_map.delete(klass) as.each do |assoc| queue << assoc.class_name # If this class is nested in the inclusion tree, only load documents # for the association above it. If there is no parent association, # we will include documents from the documents passed to this method. ds = docs ds = assoc.parent_inclusions.map { |p| docs_map[p].to_a }.flatten if assoc.parent_inclusions.length > 0 res = assoc.relation.eager_loader([ assoc ], ds).run docs_map[assoc.name] ||= [].to_set docs_map[assoc.name].merge(res) end end end
#preload_for_lookup(criteria)
Load the associations for the given documents. This will be done recursively to load the associations of the given documents’ associated documents.
# File 'lib/mongoid/association/eager_loadable.rb', line 92
def preload_for_lookup(criteria) assoc_map = criteria.inclusions.group_by(&:inverse_class_name) # match first pipeline = criteria.selector.to_pipeline # then sort, skip, limit pipeline.concat(criteria..to_pipeline_for_lookup) # account for single-collection inheritance root_class = klass.root_class if assoc_map[klass.to_s] assoc_map[klass.to_s].each do |assoc| # Create a copy of the mapping for each top-level association to avoid mutation issues pipeline << create_pipeline(assoc, assoc_map.dup) end end if klass != root_class && assoc_map[root_class.to_s] assoc_map[root_class.to_s].each do |assoc| # Create a copy of the mapping for each top-level association to avoid mutation issues pipeline << create_pipeline(assoc, assoc_map.dup) end end Eager.new(criteria.inclusions, [], true, pipeline).run end
#switch_local_and_foreign_fields?(association) ⇒ Boolean
# File 'lib/mongoid/association/eager_loadable.rb', line 142
def switch_local_and_foreign_fields?(association) association.is_a?(Mongoid::Association::Referenced::BelongsTo) || association.is_a?(Mongoid::Association::Referenced::HasAndBelongsToMany) end