Module: Mongoid::Association::EagerLoadable

Included in:
Contextual::Memory, Contextual::Mongo, Contextual::Mongo::DocumentsLoader
Defined in:
lib/mongoid/association/eager_loadable.rb

Overview

This module defines the eager loading behavior for criteria.

Instance Method Summary collapse

Instance Method Details

#create_pipeline(current_assoc, mapping) ⇒ Object



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# 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

#eager_load(docs) ⇒ Array<Mongoid::Document>

Load the associations for the given documents.

Parameters:

Returns:



22
23
24
25
26
# 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:



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 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

#eager_loadable?true | false

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

Returns:

  • (true | false)

    Whether to eager load.



13
14
15
# File 'lib/mongoid/association/eager_loadable.rb', line 13

def eager_loadable?
  !criteria.inclusions.empty?
end

#preload(associations, docs) ⇒ Object

Load the associations for the given documents. This will be done recursively to load the associations of the given documents' associated documents.

Parameters:



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# 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) ⇒ Object

Load the associations for the given documents. This will be done recursively to load the associations of the given documents' associated documents.

Parameters:



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# 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

Returns:



142
143
144
145
# 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