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