class Sequel::Model::Associations::EagerGraphLoader

This class is the internal implementation of eager_graph. It is responsible for taking an array of plain hashes and returning an array of model objects with all eager_graphed associations already set in the association cache.

Attributes

after_load_map[R]

Hash with table alias symbol keys and after_load hook values

alias_map[R]

Hash with table alias symbol keys and association name values

column_maps[R]

Hash with table alias symbol keys and subhash values mapping column_alias symbols to the symbol of the real name of the column

dependency_map[R]

Recursive hash with table alias symbol keys mapping to hashes with dependent table alias symbol keys.

limit_map[R]

Hash with table alias symbol keys and [limit, offset] values

master[R]

Hash with table alias symbol keys and callable values used to create model instances The table alias symbol for the primary model

primary_keys[R]

Hash with table alias symbol keys and primary key symbol values (or arrays of primary key symbols for composite key tables)

reciprocal_map[R]

Hash with table alias symbol keys and reciprocal association symbol values, used for setting reciprocals for one_to_many associations.

records_map[R]

Hash with table alias symbol keys and subhash values mapping primary key symbols (or array of symbols) to model instances. Used so that only a single model instance is created for each object.

reflection_map[R]

Hash with table alias symbol keys and AssociationReflection values

row_procs[R]

Hash with table alias symbol keys and callable values used to create model instances

type_map[R]

Hash with table alias symbol keys and true/false values, where true means the association represented by the table alias uses an array of values instead of a single value (i.e. true => *_many, false => *_to_one).

Public Class Methods

new(dataset) click to toggle source

Initialize all of the data structures used during loading.

# File lib/sequel/model/associations.rb, line 3182
def initialize(dataset)
  opts = dataset.opts
  eager_graph = opts[:eager_graph]
  @master =  eager_graph[:master]
  requirements = eager_graph[:requirements]
  reflection_map = @reflection_map = eager_graph[:reflections]
  reciprocal_map = @reciprocal_map = eager_graph[:reciprocals]
  limit_map = @limit_map = eager_graph[:limits]
  @unique = eager_graph[:cartesian_product_number] > 1
      
  alias_map = @alias_map = {}
  type_map = @type_map = {}
  after_load_map = @after_load_map = {}
  reflection_map.each do |k, v|
    alias_map[k] = v[:name]
    after_load_map[k] = v[:after_load] unless v[:after_load].empty?
    type_map[k] = if v.returns_array?
      true
    elsif (limit_and_offset = limit_map[k]) && !limit_and_offset.last.nil?
      :offset
    end
  end

  # Make dependency map hash out of requirements array for each association.
  # This builds a tree of dependencies that will be used for recursion
  # to ensure that all parts of the object graph are loaded into the
  # appropriate subordinate association.
  @dependency_map = {}
  # Sort the associations by requirements length, so that
  # requirements are added to the dependency hash before their
  # dependencies.
  requirements.sort_by{|a| a[1].length}.each do |ta, deps|
    if deps.empty?
      dependency_map[ta] = {}
    else
      deps = deps.dup
      hash = dependency_map[deps.shift]
      deps.each do |dep|
        hash = hash[dep]
      end
      hash[ta] = {}
    end
  end
      
  # This mapping is used to make sure that duplicate entries in the
  # result set are mapped to a single record.  For example, using a
  # single one_to_many association with 10 associated records,
  # the main object column values appear in the object graph 10 times.
  # We map by primary key, if available, or by the object's entire values,
  # if not. The mapping must be per table, so create sub maps for each table
  # alias.
  records_map = {@master=>{}}
  alias_map.keys.each{|ta| records_map[ta] = {}}
  @records_map = records_map

  datasets = opts[:graph][:table_aliases].to_a.reject{|ta,ds| ds.nil?}
  column_aliases = opts[:graph_aliases] || opts[:graph][:column_aliases]
  primary_keys = {}
  column_maps = {}
  models = {}
  row_procs = {}
  datasets.each do |ta, ds|
    models[ta] = ds.model
    primary_keys[ta] = []
    column_maps[ta] = {}
    row_procs[ta] = ds.row_proc
  end
  column_aliases.each do |col_alias, tc|
    ta, column = tc
    column_maps[ta][col_alias] = column
  end
  column_maps.each do |ta, h|
    pk = models[ta].primary_key
    if pk.is_a?(Array)
      primary_keys[ta] = []
      h.select{|ca, c| primary_keys[ta] << ca if pk.include?(c)}
    else
      h.select{|ca, c| primary_keys[ta] = ca if pk == c}
    end
  end
  @column_maps = column_maps
  @primary_keys = primary_keys
  @row_procs = row_procs

  # For performance, create two special maps for the master table,
  # so you can skip a hash lookup.
  @master_column_map = column_maps[master]
  @master_primary_keys = primary_keys[master]

  # Add a special hash mapping table alias symbols to 5 element arrays that just
  # contain the data in other data structures for that table alias.  This is
  # used for performance, to get all values in one hash lookup instead of
  # separate hash lookups for each data structure.
  ta_map = {}
  alias_map.keys.each do |ta|
    ta_map[ta] = [records_map[ta], row_procs[ta], alias_map[ta], type_map[ta], reciprocal_map[ta]]
  end
  @ta_map = ta_map
end

Public Instance Methods

load(hashes) click to toggle source

Return an array of primary model instances with the associations cache prepopulated for all model objects (both primary and associated).

# File lib/sequel/model/associations.rb, line 3284
def load(hashes)
  master = master()
      
  # Assign to local variables for speed increase
  rp = row_procs[master]
  rm = records_map[master]
  dm = dependency_map

  # This will hold the final record set that we will be replacing the object graph with.
  records = []

  hashes.each do |h|
    unless key = master_pk(h)
      key = hkey(master_hfor(h))
    end
    unless primary_record = rm[key]
      primary_record = rm[key] = rp.call(master_hfor(h))
      # Only add it to the list of records to return if it is a new record
      records.push(primary_record)
    end
    # Build all associations for the current object and it's dependencies
    _load(dm, primary_record, h)
  end
      
  # Remove duplicate records from all associations if this graph could possibly be a cartesian product
  # Run after_load procs if there are any
  post_process(records, dm) if @unique || !after_load_map.empty? || !limit_map.empty?

  records
end

Private Instance Methods

_load(dependency_map, current, h) click to toggle source

Recursive method that creates associated model objects and associates them to the current model object.

# File lib/sequel/model/associations.rb, line 3318
def _load(dependency_map, current, h)
  dependency_map.each do |ta, deps|
    unless key = pk(ta, h)
      ta_h = hfor(ta, h)
      unless ta_h.values.any?
        assoc_name = alias_map[ta]
        unless (assoc = current.associations).has_key?(assoc_name)
          assoc[assoc_name] = type_map[ta] ? [] : nil
        end
        next
      end
      key = hkey(ta_h)
    end
    rm, rp, assoc_name, tm, rcm = @ta_map[ta]
    unless rec = rm[key]
      rec = rm[key] = rp.call(hfor(ta, h))
    end

    if tm
      unless (assoc = current.associations).has_key?(assoc_name)
        assoc[assoc_name] = []
      end
      assoc[assoc_name].push(rec) 
      rec.associations[rcm] = current if rcm
    else
      current.associations[assoc_name] ||= rec
    end
    # Recurse into dependencies of the current object
    _load(deps, rec, h) unless deps.empty?
  end
end
hfor(ta, h) click to toggle source

Return the subhash for the specific table alias ta by parsing the values out of the main hash h

# File lib/sequel/model/associations.rb, line 3351
def hfor(ta, h)
  out = {}
  @column_maps[ta].each{|ca, c| out[c] = h[ca]}
  out
end
hkey(h) click to toggle source

Return a suitable hash key for any subhash h, which is an array of values by column order. This is only used if the primary key cannot be used.

# File lib/sequel/model/associations.rb, line 3359
def hkey(h)
  h.sort_by{|x| x[0].to_s}
end
master_hfor(h) click to toggle source

Return the subhash for the master table by parsing the values out of the main hash h

# File lib/sequel/model/associations.rb, line 3364
def master_hfor(h)
  out = {}
  @master_column_map.each{|ca, c| out[c] = h[ca]}
  out
end
master_pk(h) click to toggle source

Return a primary key value for the master table by parsing it out of the main hash h.

# File lib/sequel/model/associations.rb, line 3371
def master_pk(h)
  x = @master_primary_keys
  if x.is_a?(Array)
    unless x == []
      x = x.map{|ca| h[ca]}
      x if x.all?
    end
  else
    h[x]
  end
end
pk(ta, h) click to toggle source

Return a primary key value for the given table alias by parsing it out of the main hash h.

# File lib/sequel/model/associations.rb, line 3384
def pk(ta, h)
  x = primary_keys[ta]
  if x.is_a?(Array)
    unless x == []
      x = x.map{|ca| h[ca]}
      x if x.all?
    end
  else
    h[x]
  end
end
post_process(records, dependency_map) click to toggle source

If the result set is the result of a cartesian product, then it is possible that there are multiple records for each association when there should only be one. In that case, for each object in all associations loaded via eager_graph, run uniq! on the association to make sure no duplicate records show up. Note that this can cause legitimate duplicate records to be removed.

# File lib/sequel/model/associations.rb, line 3401
def post_process(records, dependency_map)
  records.each do |record|
    dependency_map.each do |ta, deps|
      assoc_name = alias_map[ta]
      list = record.send(assoc_name)
      rec_list = if type_map[ta]
        list.uniq!
        if lo = limit_map[ta]
          limit, offset = lo
          offset ||= 0
          if type_map[ta] == :offset
            [record.associations[assoc_name] = list[offset]]
          else
            list.replace(list[(offset)..(limit ? (offset)+limit-1 : -1)] || [])
          end
        else
          list
        end
      elsif list
        [list]
      else
        []
      end
      record.send(:run_association_callbacks, reflection_map[ta], :after_load, list) if after_load_map[ta]
      post_process(rec_list, deps) if !rec_list.empty? && !deps.empty?
    end
  end
end