123456789_123456789_123456789_123456789_123456789_

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

Instance Method Summary

Instance Attribute Details

#eager_loadable?true | false (readonly)

Indicates whether the criteria has association inclusions which should be eager loaded.

Returns:

  • (true | false)

    Whether to eager load.

[ GitHub ]

  
# 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_inclusionsArray<Mongoid::Association::Relatable> (private)

Returns the inclusions whose target class resides in a different cluster than the root class.

Returns:

[ GitHub ]

  
# 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_fallbackArray<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.

Returns:

Raises:

  • (NotImplementedError)
[ GitHub ]

  
# 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.

Parameters:

Returns:

[ GitHub ]

  
# 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_lookupArray<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.

Returns:

[ GitHub ]

  
# 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.

Parameters:

[ GitHub ]

  
# 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.

Parameters:

[ GitHub ]

  
# 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.options.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

[ GitHub ]

  
# 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