Module: Mongoid::Association::EagerLoadable
- Defined in:
- lib/mongoid/association/eager_loadable.rb
Overview
This module defines the eager loading behavior for criteria.
Instance Method Summary collapse
- #create_pipeline(current_assoc, mapping) ⇒ Object
-
#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.
-
#eager_loadable? ⇒ true | false
Indicates whether the criteria has association inclusions which should be eager loaded.
-
#preload(associations, docs) ⇒ Object
Load the associations for the given documents.
-
#preload_for_lookup(criteria) ⇒ Object
Load the associations for the given documents.
- #switch_local_and_foreign_fields?(association) ⇒ Boolean
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.
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_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.
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.
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.
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.
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..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
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 |