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
3785 def initialize(dataset)
3786   opts = dataset.opts
3787   eager_graph = opts[:eager_graph]
3788   @master =  eager_graph[:master]
3789   requirements = eager_graph[:requirements]
3790   reflection_map = @reflection_map = eager_graph[:reflections]
3791   reciprocal_map = @reciprocal_map = eager_graph[:reciprocals]
3792   limit_map = @limit_map = eager_graph[:limits]
3793   @unique = eager_graph[:cartesian_product_number] > 1
3794       
3795   alias_map = @alias_map = {}
3796   type_map = @type_map = {}
3797   after_load_map = @after_load_map = {}
3798   reflection_map.each do |k, v|
3799     alias_map[k] = v[:name]
3800     after_load_map[k] = v[:after_load] if v[:after_load]
3801     type_map[k] = if v.returns_array?
3802       true
3803     elsif (limit_and_offset = limit_map[k]) && !limit_and_offset.last.nil?
3804       :offset
3805     end
3806   end
3807   after_load_map.freeze
3808   alias_map.freeze
3809   type_map.freeze
3810 
3811   # Make dependency map hash out of requirements array for each association.
3812   # This builds a tree of dependencies that will be used for recursion
3813   # to ensure that all parts of the object graph are loaded into the
3814   # appropriate subordinate association.
3815   dependency_map = @dependency_map = {}
3816   # Sort the associations by requirements length, so that
3817   # requirements are added to the dependency hash before their
3818   # dependencies.
3819   requirements.sort_by{|a| a[1].length}.each do |ta, deps|
3820     if deps.empty?
3821       dependency_map[ta] = {}
3822     else
3823       deps = deps.dup
3824       hash = dependency_map[deps.shift]
3825       deps.each do |dep|
3826         hash = hash[dep]
3827       end
3828       hash[ta] = {}
3829     end
3830   end
3831   freezer = lambda do |h|
3832     h.freeze
3833     h.each_value(&freezer)
3834   end
3835   freezer.call(dependency_map)
3836       
3837   datasets = opts[:graph][:table_aliases].to_a.reject{|ta,ds| ds.nil?}
3838   column_aliases = opts[:graph][:column_aliases]
3839   primary_keys = {}
3840   column_maps = {}
3841   models = {}
3842   row_procs = {}
3843   datasets.each do |ta, ds|
3844     models[ta] = ds.model
3845     primary_keys[ta] = []
3846     column_maps[ta] = {}
3847     row_procs[ta] = ds.row_proc
3848   end
3849   column_aliases.each do |col_alias, tc|
3850     ta, column = tc
3851     column_maps[ta][col_alias] = column
3852   end
3853   column_maps.each do |ta, h|
3854     pk = models[ta].primary_key
3855     if pk.is_a?(Array)
3856       primary_keys[ta] = []
3857       h.select{|ca, c| primary_keys[ta] << ca if pk.include?(c)}
3858     else
3859       h.select{|ca, c| primary_keys[ta] = ca if pk == c}
3860     end
3861   end
3862   @column_maps = column_maps.freeze
3863   @primary_keys = primary_keys.freeze
3864   @row_procs = row_procs.freeze
3865 
3866   # For performance, create two special maps for the master table,
3867   # so you can skip a hash lookup.
3868   @master_column_map = column_maps[master]
3869   @master_primary_keys = primary_keys[master]
3870 
3871   # Add a special hash mapping table alias symbols to 5 element arrays that just
3872   # contain the data in other data structures for that table alias.  This is
3873   # used for performance, to get all values in one hash lookup instead of
3874   # separate hash lookups for each data structure.
3875   ta_map = {}
3876   alias_map.each_key do |ta|
3877     ta_map[ta] = [row_procs[ta], alias_map[ta], type_map[ta], reciprocal_map[ta]].freeze
3878   end
3879   @ta_map = ta_map.freeze
3880   freeze
3881 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
3885 def load(hashes)
3886   # This mapping is used to make sure that duplicate entries in the
3887   # result set are mapped to a single record.  For example, using a
3888   # single one_to_many association with 10 associated records,
3889   # the main object column values appear in the object graph 10 times.
3890   # We map by primary key, if available, or by the object's entire values,
3891   # if not. The mapping must be per table, so create sub maps for each table
3892   # alias.
3893   @records_map = records_map = {}
3894   alias_map.keys.each{|ta| records_map[ta] = {}}
3895 
3896   master = master()
3897       
3898   # Assign to local variables for speed increase
3899   rp = row_procs[master]
3900   rm = records_map[master] = {}
3901   dm = dependency_map
3902 
3903   records_map.freeze
3904 
3905   # This will hold the final record set that we will be replacing the object graph with.
3906   records = []
3907 
3908   hashes.each do |h|
3909     unless key = master_pk(h)
3910       key = hkey(master_hfor(h))
3911     end
3912     unless primary_record = rm[key]
3913       primary_record = rm[key] = rp.call(master_hfor(h))
3914       # Only add it to the list of records to return if it is a new record
3915       records.push(primary_record)
3916     end
3917     # Build all associations for the current object and it's dependencies
3918     _load(dm, primary_record, h)
3919   end
3920       
3921   # Remove duplicate records from all associations if this graph could possibly be a cartesian product
3922   # Run after_load procs if there are any
3923   post_process(records, dm) if @unique || !after_load_map.empty? || !limit_map.empty?
3924 
3925   records_map.each_value(&:freeze)
3926   freeze
3927 
3928   records
3929 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
3934 def _load(dependency_map, current, h)
3935   dependency_map.each do |ta, deps|
3936     unless key = pk(ta, h)
3937       ta_h = hfor(ta, h)
3938       unless ta_h.values.any?
3939         assoc_name = alias_map[ta]
3940         unless (assoc = current.associations).has_key?(assoc_name)
3941           assoc[assoc_name] = type_map[ta] ? [] : nil
3942         end
3943         next
3944       end
3945       key = hkey(ta_h)
3946     end
3947     rp, assoc_name, tm, rcm = @ta_map[ta]
3948     rm = records_map[ta]
3949 
3950     # Check type map for all dependencies, and use a unique
3951     # object if any are dependencies for multiple objects,
3952     # to prevent duplicate objects from showing up in the case
3953     # the normal duplicate removal code is not being used.
3954     if !@unique && !deps.empty? && deps.any?{|dep_key,_| @ta_map[dep_key][2]}
3955       key = [current.object_id, key]
3956     end
3957 
3958     unless rec = rm[key]
3959       rec = rm[key] = rp.call(hfor(ta, h))
3960     end
3961 
3962     if tm
3963       unless (assoc = current.associations).has_key?(assoc_name)
3964         assoc[assoc_name] = []
3965       end
3966       assoc[assoc_name].push(rec) 
3967       rec.associations[rcm] = current if rcm
3968     else
3969       current.associations[assoc_name] ||= rec
3970     end
3971     # Recurse into dependencies of the current object
3972     _load(deps, rec, h) unless deps.empty?
3973   end
3974 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
3977 def hfor(ta, h)
3978   out = {}
3979   @column_maps[ta].each{|ca, c| out[c] = h[ca]}
3980   out
3981 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
3985 def hkey(h)
3986   h.sort_by{|x| x[0]}
3987 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
3990 def master_hfor(h)
3991   out = {}
3992   @master_column_map.each{|ca, c| out[c] = h[ca]}
3993   out
3994 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
3997 def master_pk(h)
3998   x = @master_primary_keys
3999   if x.is_a?(Array)
4000     unless x == []
4001       x = x.map{|ca| h[ca]}
4002       x if x.all?
4003     end
4004   else
4005     h[x]
4006   end
4007 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
4010 def pk(ta, h)
4011   x = primary_keys[ta]
4012   if x.is_a?(Array)
4013     unless x == []
4014       x = x.map{|ca| h[ca]}
4015       x if x.all?
4016     end
4017   else
4018     h[x]
4019   end
4020 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
4027 def post_process(records, dependency_map)
4028   records.each do |record|
4029     dependency_map.each do |ta, deps|
4030       assoc_name = alias_map[ta]
4031       list = record.public_send(assoc_name)
4032       rec_list = if type_map[ta]
4033         list.uniq!
4034         if lo = limit_map[ta]
4035           limit, offset = lo
4036           offset ||= 0
4037           if type_map[ta] == :offset
4038             [record.associations[assoc_name] = list[offset]]
4039           else
4040             list.replace(list[(offset)..(limit ? (offset)+limit-1 : -1)] || [])
4041           end
4042         else
4043           list
4044         end
4045       elsif list
4046         [list]
4047       else
4048         []
4049       end
4050       record.send(:run_association_callbacks, reflection_map[ta], :after_load, list) if after_load_map[ta]
4051       post_process(rec_list, deps) if !rec_list.empty? && !deps.empty?
4052     end
4053   end
4054 end