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