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