123456789_123456789_123456789_123456789_123456789_

Class: Mongoid::Association::EagerLoad::EmbeddedDistributor Private

Relationships & Source Files
Inherits: Object
Defined in: lib/mongoid/association/eager_load/embedded_distributor.rb

Overview

Distributes the results of a $lookup onto the embedded documents they belong to.

A $lookup overwrites the single field it writes to, and it can't distribute its matches across the elements of an embedded array. So when the reference being eager-loaded lives on an embedded document, the matches are first collected in a temporary top-level field and then distributed down the embedded path onto each embedded document, merging into it (so the rest of the document is kept) and correlating by key. The temporary field is then dropped.

For Computer.eager_load(port: :device) (Port belongs_to :device) it emits:

{ '$lookup' => {                     # devices can't be written into
'from' => 'devices',               # the embedded port, so they are
'localField' => 'port.device_id',  # collected in a temp top-level
'foreignField' => '_id',           # field instead
'as' => '__eager_load_port_device'
} },
{ '$set' => {
'port' => { '$mergeObjects' => [   # merge a 'device' key onto the port
  '$port',
  { 'device' => { '$filter' => { ... } } }   # this port's matches
] }
} },
{ '$unset' => '__eager_load_port_device' }  # drop the temp field

Class Method Summary

Instance Method Summary

  • #stages ⇒ Array<Hash> Internal use only

    The stages that run the $lookup into a temporary field and then distribute its matches onto the embedded documents along the path.

  • #correlated_matches(element) private Internal use only

    The matches that belong to a single embedded element.

  • #distributed_value(chain, node) private Internal use only

    An embedded collection (embeds_many) is rebuilt with $map so each element keeps its own matches instead of collapsing onto the first; a single embedded document (embeds_one) receives its matches in place.

  • #match_operator private Internal use only

    A has_and_belongs_to_many holds an array of foreign keys, so a match belongs when its key is among them ($in); every other association points at a single key ($eq).

  • #merge_into_present(node, merged) private Internal use only

    Merge the matches into a single embedded document only when it exists, so an absent embeds_one stays absent instead of being synthesized from its matches alone.

  • #path private Internal use only
  • #redirect_lookup_to_temporary_field private Internal use only

    The $lookup runs at the top level, so it reads the local field by its full embedded path and writes the matches into the temporary field.

  • #root private Internal use only
  • #temporary_field private Internal use only

Constructor Details

.new(association, chain, lookup_stage, local_field, foreign_field) ⇒ EmbeddedDistributor

[ GitHub ]

  
# File 'lib/mongoid/association/eager_load/embedded_distributor.rb', line 53

def initialize(association, chain, lookup_stage, local_field, foreign_field)
  @association = association
  @chain = chain
  @lookup_stage = lookup_stage
  @local_field = local_field
  @foreign_field = foreign_field
end

Class Method Details

.for(association:, chain:, lookup_stage:)

[ GitHub ]

  
# File 'lib/mongoid/association/eager_load/embedded_distributor.rb', line 45

def for(association:, chain:, lookup_stage:)
  lookup = lookup_stage['$lookup']
  new(association, chain, lookup_stage, lookup['localField'], lookup['foreignField'])
end

Instance Method Details

#correlated_matches(element) (private)

The matches that belong to a single embedded element.

[ GitHub ]

  
# File 'lib/mongoid/association/eager_load/embedded_distributor.rb', line 134

def correlated_matches(element)
  { '$filter' => {
    'input' => "$#{temporary_field}",
    'as' => 'match',
    'cond' => { match_operator => [ "$$match.#{@foreign_field}", "#{element}.#{@local_field}" ] }
  } }
end

#distributed_value(chain, node) (private)

An embedded collection (embeds_many) is rebuilt with $map so each element keeps its own matches instead of collapsing onto the first; a single embedded document (embeds_one) receives its matches in place.

[ GitHub ]

  
# File 'lib/mongoid/association/eager_load/embedded_distributor.rb', line 101

def distributed_value(chain, node)
  head, *rest = chain
  many = head.many?
  element = many ? "$$#{head.store_as}" : node
  child =
    if rest.empty?
      { @association.name.to_s => correlated_matches(element) }
    else
      segment = rest.first.store_as
      { segment => distributed_value(rest, "#{element}.#{segment}") }
    end
  merged = { '$mergeObjects' => [ element, child ] }
  return merge_into_present(node, merged) unless many

  { '$map' => {
    'input' => node,
    'as' => head.store_as,
    'in' => merged
  } }
end

#match_operator (private)

A has_and_belongs_to_many holds an array of foreign keys, so a match belongs when its key is among them ($in); every other association points at a single key ($eq).

[ GitHub ]

  
# File 'lib/mongoid/association/eager_load/embedded_distributor.rb', line 145

def match_operator
  @association.many_to_many? ? '$in' : '$eq'
end

#merge_into_present(node, merged) (private)

Merge the matches into a single embedded document only when it exists, so an absent embeds_one stays absent instead of being synthesized from its matches alone.

[ GitHub ]

  
# File 'lib/mongoid/association/eager_load/embedded_distributor.rb', line 125

def merge_into_present(node, merged)
  { '$cond' => {
    'if' => { '$ifNull' => [ node, false ] },
    'then' => merged,
    'else' => node
  } }
end

#path (private)

[ GitHub ]

  
# File 'lib/mongoid/association/eager_load/embedded_distributor.rb', line 86

def path
  @chain.map(&:store_as).join('.')
end

#redirect_lookup_to_temporary_field (private)

The $lookup runs at the top level, so it reads the local field by its full embedded path and writes the matches into the temporary field.

[ GitHub ]

  
# File 'lib/mongoid/association/eager_load/embedded_distributor.rb', line 80

def redirect_lookup_to_temporary_field
  lookup = @lookup_stage['$lookup']
  lookup['localField'] = "#{path}.#{@local_field}"
  lookup['as'] = temporary_field
end

#root (private)

[ GitHub ]

  
# File 'lib/mongoid/association/eager_load/embedded_distributor.rb', line 90

def root
  @chain.first.store_as
end

#stagesArray<Hash>

The stages that run the $lookup into a temporary field and then distribute its matches onto the embedded documents along the path.

Returns:

  • (Array<Hash>)

    The stages to append to the pipeline.

[ GitHub ]

  
# File 'lib/mongoid/association/eager_load/embedded_distributor.rb', line 65

def stages
  redirect_lookup_to_temporary_field
  [
    @lookup_stage,
    { '$set' => {
      root => distributed_value(@chain, "$#{root}")
    } },
    { '$unset' => temporary_field }
  ]
end

#temporary_field (private)

[ GitHub ]

  
# File 'lib/mongoid/association/eager_load/embedded_distributor.rb', line 94

def temporary_field
  "__eager_load_#{path.tr('.', '_')}_#{@association.name}"
end