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]

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
3518 def initialize(dataset)
3519   opts = dataset.opts
3520   eager_graph = opts[:eager_graph]
3521   @master =  eager_graph[:master]
3522   requirements = eager_graph[:requirements]
3523   reflection_map = @reflection_map = eager_graph[:reflections]
3524   reciprocal_map = @reciprocal_map = eager_graph[:reciprocals]
3525   limit_map = @limit_map = eager_graph[:limits]
3526   @unique = eager_graph[:cartesian_product_number] > 1
3527       
3528   alias_map = @alias_map = {}
3529   type_map = @type_map = {}
3530   after_load_map = @after_load_map = {}
3531   reflection_map.each do |k, v|
3532     alias_map[k] = v[:name]
3533     after_load_map[k] = v[:after_load] if v[:after_load]
3534     type_map[k] = if v.returns_array?
3535       true
3536     elsif (limit_and_offset = limit_map[k]) && !limit_and_offset.last.nil?
3537       :offset
3538     end
3539   end
3540   after_load_map.freeze
3541   alias_map.freeze
3542   type_map.freeze
3543 
3544   # Make dependency map hash out of requirements array for each association.
3545   # This builds a tree of dependencies that will be used for recursion
3546   # to ensure that all parts of the object graph are loaded into the
3547   # appropriate subordinate association.
3548   dependency_map = @dependency_map = {}
3549   # Sort the associations by requirements length, so that
3550   # requirements are added to the dependency hash before their
3551   # dependencies.
3552   requirements.sort_by{|a| a[1].length}.each do |ta, deps|
3553     if deps.empty?
3554       dependency_map[ta] = {}
3555     else
3556       deps = deps.dup
3557       hash = dependency_map[deps.shift]
3558       deps.each do |dep|
3559         hash = hash[dep]
3560       end
3561       hash[ta] = {}
3562     end
3563   end
3564   freezer = lambda do |h|
3565     h.freeze
3566     h.each_value(&freezer)
3567   end
3568   freezer.call(dependency_map)
3569       
3570   datasets = opts[:graph][:table_aliases].to_a.reject{|ta,ds| ds.nil?}
3571   column_aliases = opts[:graph][:column_aliases]
3572   primary_keys = {}
3573   column_maps = {}
3574   models = {}
3575   row_procs = {}
3576   datasets.each do |ta, ds|
3577     models[ta] = ds.model
3578     primary_keys[ta] = []
3579     column_maps[ta] = {}
3580     row_procs[ta] = ds.row_proc
3581   end
3582   column_aliases.each do |col_alias, tc|
3583     ta, column = tc
3584     column_maps[ta][col_alias] = column
3585   end
3586   column_maps.each do |ta, h|
3587     pk = models[ta].primary_key
3588     if pk.is_a?(Array)
3589       primary_keys[ta] = []
3590       h.select{|ca, c| primary_keys[ta] << ca if pk.include?(c)}
3591     else
3592       h.select{|ca, c| primary_keys[ta] = ca if pk == c}
3593     end
3594   end
3595   @column_maps = column_maps.freeze
3596   @primary_keys = primary_keys.freeze
3597   @row_procs = row_procs.freeze
3598 
3599   # For performance, create two special maps for the master table,
3600   # so you can skip a hash lookup.
3601   @master_column_map = column_maps[master]
3602   @master_primary_keys = primary_keys[master]
3603 
3604   # Add a special hash mapping table alias symbols to 5 element arrays that just
3605   # contain the data in other data structures for that table alias.  This is
3606   # used for performance, to get all values in one hash lookup instead of
3607   # separate hash lookups for each data structure.
3608   ta_map = {}
3609   alias_map.each_key do |ta|
3610     ta_map[ta] = [row_procs[ta], alias_map[ta], type_map[ta], reciprocal_map[ta]].freeze
3611   end
3612   @ta_map = ta_map.freeze
3613   freeze
3614 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
3618 def load(hashes)
3619   # This mapping is used to make sure that duplicate entries in the
3620   # result set are mapped to a single record.  For example, using a
3621   # single one_to_many association with 10 associated records,
3622   # the main object column values appear in the object graph 10 times.
3623   # We map by primary key, if available, or by the object's entire values,
3624   # if not. The mapping must be per table, so create sub maps for each table
3625   # alias.
3626   @records_map = records_map = {}
3627   alias_map.keys.each{|ta| records_map[ta] = {}}
3628 
3629   master = master()
3630       
3631   # Assign to local variables for speed increase
3632   rp = row_procs[master]
3633   rm = records_map[master] = {}
3634   dm = dependency_map
3635 
3636   records_map.freeze
3637 
3638   # This will hold the final record set that we will be replacing the object graph with.
3639   records = []
3640 
3641   hashes.each do |h|
3642     unless key = master_pk(h)
3643       key = hkey(master_hfor(h))
3644     end
3645     unless primary_record = rm[key]
3646       primary_record = rm[key] = rp.call(master_hfor(h))
3647       # Only add it to the list of records to return if it is a new record
3648       records.push(primary_record)
3649     end
3650     # Build all associations for the current object and it's dependencies
3651     _load(dm, primary_record, h)
3652   end
3653       
3654   # Remove duplicate records from all associations if this graph could possibly be a cartesian product
3655   # Run after_load procs if there are any
3656   post_process(records, dm) if @unique || !after_load_map.empty? || !limit_map.empty?
3657 
3658   records_map.each_value(&:freeze)
3659   freeze
3660 
3661   records
3662 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
3667 def _load(dependency_map, current, h)
3668   dependency_map.each do |ta, deps|
3669     unless key = pk(ta, h)
3670       ta_h = hfor(ta, h)
3671       unless ta_h.values.any?
3672         assoc_name = alias_map[ta]
3673         unless (assoc = current.associations).has_key?(assoc_name)
3674           assoc[assoc_name] = type_map[ta] ? [] : nil
3675         end
3676         next
3677       end
3678       key = hkey(ta_h)
3679     end
3680     rp, assoc_name, tm, rcm = @ta_map[ta]
3681     rm = records_map[ta]
3682 
3683     # Check type map for all dependencies, and use a unique
3684     # object if any are dependencies for multiple objects,
3685     # to prevent duplicate objects from showing up in the case
3686     # the normal duplicate removal code is not being used.
3687     if !@unique && !deps.empty? && deps.any?{|dep_key,_| @ta_map[dep_key][2]}
3688       key = [current.object_id, key]
3689     end
3690 
3691     unless rec = rm[key]
3692       rec = rm[key] = rp.call(hfor(ta, h))
3693     end
3694 
3695     if tm
3696       unless (assoc = current.associations).has_key?(assoc_name)
3697         assoc[assoc_name] = []
3698       end
3699       assoc[assoc_name].push(rec) 
3700       rec.associations[rcm] = current if rcm
3701     else
3702       current.associations[assoc_name] ||= rec
3703     end
3704     # Recurse into dependencies of the current object
3705     _load(deps, rec, h) unless deps.empty?
3706   end
3707 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
3710 def hfor(ta, h)
3711   out = {}
3712   @column_maps[ta].each{|ca, c| out[c] = h[ca]}
3713   out
3714 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
3718 def hkey(h)
3719   h.sort_by{|x| x[0]}
3720 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
3723 def master_hfor(h)
3724   out = {}
3725   @master_column_map.each{|ca, c| out[c] = h[ca]}
3726   out
3727 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
3730 def master_pk(h)
3731   x = @master_primary_keys
3732   if x.is_a?(Array)
3733     unless x == []
3734       x = x.map{|ca| h[ca]}
3735       x if x.all?
3736     end
3737   else
3738     h[x]
3739   end
3740 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
3743 def pk(ta, h)
3744   x = primary_keys[ta]
3745   if x.is_a?(Array)
3746     unless x == []
3747       x = x.map{|ca| h[ca]}
3748       x if x.all?
3749     end
3750   else
3751     h[x]
3752   end
3753 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
3760 def post_process(records, dependency_map)
3761   records.each do |record|
3762     dependency_map.each do |ta, deps|
3763       assoc_name = alias_map[ta]
3764       list = record.public_send(assoc_name)
3765       rec_list = if type_map[ta]
3766         list.uniq!
3767         if lo = limit_map[ta]
3768           limit, offset = lo
3769           offset ||= 0
3770           if type_map[ta] == :offset
3771             [record.associations[assoc_name] = list[offset]]
3772           else
3773             list.replace(list[(offset)..(limit ? (offset)+limit-1 : -1)] || [])
3774           end
3775         else
3776           list
3777         end
3778       elsif list
3779         [list]
3780       else
3781         []
3782       end
3783       record.send(:run_association_callbacks, reflection_map[ta], :after_load, list) if after_load_map[ta]
3784       post_process(rec_list, deps) if !rec_list.empty? && !deps.empty?
3785     end
3786   end
3787 end