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