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