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
3968 def initialize(dataset)
3969   opts = dataset.opts
3970   eager_graph = opts[:eager_graph]
3971   @master =  eager_graph[:master]
3972   requirements = eager_graph[:requirements]
3973   reflection_map = @reflection_map = eager_graph[:reflections]
3974   reciprocal_map = @reciprocal_map = eager_graph[:reciprocals]
3975   limit_map = @limit_map = eager_graph[:limits]
3976   @unique = eager_graph[:cartesian_product_number] > 1
3977       
3978   alias_map = @alias_map = {}
3979   type_map = @type_map = {}
3980   after_load_map = @after_load_map = {}
3981   reflection_map.each do |k, v|
3982     alias_map[k] = v[:name]
3983     after_load_map[k] = v[:after_load] if v[:after_load]
3984     type_map[k] = if v.returns_array?
3985       true
3986     elsif (limit_and_offset = limit_map[k]) && !limit_and_offset.last.nil?
3987       :offset
3988     end
3989   end
3990   after_load_map.freeze
3991   alias_map.freeze
3992   type_map.freeze
3993 
3994   # Make dependency map hash out of requirements array for each association.
3995   # This builds a tree of dependencies that will be used for recursion
3996   # to ensure that all parts of the object graph are loaded into the
3997   # appropriate subordinate association.
3998   dependency_map = @dependency_map = {}
3999   # Sort the associations by requirements length, so that
4000   # requirements are added to the dependency hash before their
4001   # dependencies.
4002   requirements.sort_by{|a| a[1].length}.each do |ta, deps|
4003     if deps.empty?
4004       dependency_map[ta] = {}
4005     else
4006       deps = deps.dup
4007       hash = dependency_map[deps.shift]
4008       deps.each do |dep|
4009         hash = hash[dep]
4010       end
4011       hash[ta] = {}
4012     end
4013   end
4014   freezer = lambda do |h|
4015     h.freeze
4016     h.each_value(&freezer)
4017   end
4018   freezer.call(dependency_map)
4019       
4020   datasets = opts[:graph][:table_aliases].to_a.reject{|ta,ds| ds.nil?}
4021   column_aliases = opts[:graph][:column_aliases]
4022   primary_keys = {}
4023   column_maps = {}
4024   models = {}
4025   row_procs = {}
4026   datasets.each do |ta, ds|
4027     models[ta] = ds.model
4028     primary_keys[ta] = []
4029     column_maps[ta] = {}
4030     row_procs[ta] = ds.row_proc
4031   end
4032   column_aliases.each do |col_alias, tc|
4033     ta, column = tc
4034     column_maps[ta][col_alias] = column
4035   end
4036   column_maps.each do |ta, h|
4037     pk = models[ta].primary_key
4038     if pk.is_a?(Array)
4039       primary_keys[ta] = []
4040       h.select{|ca, c| primary_keys[ta] << ca if pk.include?(c)}
4041     else
4042       h.select{|ca, c| primary_keys[ta] = ca if pk == c}
4043     end
4044   end
4045   @column_maps = column_maps.freeze
4046   @primary_keys = primary_keys.freeze
4047   @row_procs = row_procs.freeze
4048 
4049   # For performance, create two special maps for the master table,
4050   # so you can skip a hash lookup.
4051   @master_column_map = column_maps[master]
4052   @master_primary_keys = primary_keys[master]
4053 
4054   # Add a special hash mapping table alias symbols to 5 element arrays that just
4055   # contain the data in other data structures for that table alias.  This is
4056   # used for performance, to get all values in one hash lookup instead of
4057   # separate hash lookups for each data structure.
4058   ta_map = {}
4059   alias_map.each_key do |ta|
4060     ta_map[ta] = [row_procs[ta], alias_map[ta], type_map[ta], reciprocal_map[ta]].freeze
4061   end
4062   @ta_map = ta_map.freeze
4063   freeze
4064 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
4068 def load(hashes)
4069   # This mapping is used to make sure that duplicate entries in the
4070   # result set are mapped to a single record.  For example, using a
4071   # single one_to_many association with 10 associated records,
4072   # the main object column values appear in the object graph 10 times.
4073   # We map by primary key, if available, or by the object's entire values,
4074   # if not. The mapping must be per table, so create sub maps for each table
4075   # alias.
4076   @records_map = records_map = {}
4077   alias_map.keys.each{|ta| records_map[ta] = {}}
4078 
4079   master = master()
4080       
4081   # Assign to local variables for speed increase
4082   rp = row_procs[master]
4083   rm = records_map[master] = {}
4084   dm = dependency_map
4085 
4086   records_map.freeze
4087 
4088   # This will hold the final record set that we will be replacing the object graph with.
4089   records = []
4090 
4091   hashes.each do |h|
4092     unless key = master_pk(h)
4093       key = hkey(master_hfor(h))
4094     end
4095     unless primary_record = rm[key]
4096       primary_record = rm[key] = rp.call(master_hfor(h))
4097       # Only add it to the list of records to return if it is a new record
4098       records.push(primary_record)
4099     end
4100     # Build all associations for the current object and it's dependencies
4101     _load(dm, primary_record, h)
4102   end
4103       
4104   # Remove duplicate records from all associations if this graph could possibly be a cartesian product
4105   # Run after_load procs if there are any
4106   post_process(records, dm) if @unique || !after_load_map.empty? || !limit_map.empty?
4107 
4108   records_map.each_value(&:freeze)
4109   freeze
4110 
4111   records
4112 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
4117 def _load(dependency_map, current, h)
4118   dependency_map.each do |ta, deps|
4119     unless key = pk(ta, h)
4120       ta_h = hfor(ta, h)
4121       unless ta_h.values.any?
4122         assoc_name = alias_map[ta]
4123         unless (assoc = current.associations).has_key?(assoc_name)
4124           assoc[assoc_name] = type_map[ta] ? [] : nil
4125         end
4126         next
4127       end
4128       key = hkey(ta_h)
4129     end
4130     rp, assoc_name, tm, rcm = @ta_map[ta]
4131     rm = records_map[ta]
4132 
4133     # Check type map for all dependencies, and use a unique
4134     # object if any are dependencies for multiple objects,
4135     # to prevent duplicate objects from showing up in the case
4136     # the normal duplicate removal code is not being used.
4137     if !@unique && !deps.empty? && deps.any?{|dep_key,_| @ta_map[dep_key][2]}
4138       key = [current.object_id, key]
4139     end
4140 
4141     unless rec = rm[key]
4142       rec = rm[key] = rp.call(hfor(ta, h))
4143     end
4144 
4145     if tm
4146       unless (assoc = current.associations).has_key?(assoc_name)
4147         assoc[assoc_name] = []
4148       end
4149       assoc[assoc_name].push(rec) 
4150       rec.associations[rcm] = current if rcm
4151     else
4152       current.associations[assoc_name] ||= rec
4153     end
4154     # Recurse into dependencies of the current object
4155     _load(deps, rec, h) unless deps.empty?
4156   end
4157 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
4160 def hfor(ta, h)
4161   out = {}
4162   @column_maps[ta].each{|ca, c| out[c] = h[ca]}
4163   out
4164 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
4168 def hkey(h)
4169   h.sort_by{|x| x[0]}
4170 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
4173 def master_hfor(h)
4174   out = {}
4175   @master_column_map.each{|ca, c| out[c] = h[ca]}
4176   out
4177 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
4180 def master_pk(h)
4181   x = @master_primary_keys
4182   if x.is_a?(Array)
4183     unless x == []
4184       x = x.map{|ca| h[ca]}
4185       x if x.all?
4186     end
4187   else
4188     h[x]
4189   end
4190 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
4193 def pk(ta, h)
4194   x = primary_keys[ta]
4195   if x.is_a?(Array)
4196     unless x == []
4197       x = x.map{|ca| h[ca]}
4198       x if x.all?
4199     end
4200   else
4201     h[x]
4202   end
4203 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
4210 def post_process(records, dependency_map)
4211   records.each do |record|
4212     dependency_map.each do |ta, deps|
4213       assoc_name = alias_map[ta]
4214       list = record.public_send(assoc_name)
4215       rec_list = if type_map[ta]
4216         list.uniq!
4217         if lo = limit_map[ta]
4218           limit, offset = lo
4219           offset ||= 0
4220           if type_map[ta] == :offset
4221             [record.associations[assoc_name] = list[offset]]
4222           else
4223             list.replace(list[(offset)..(limit ? (offset)+limit-1 : -1)] || [])
4224           end
4225         else
4226           list
4227         end
4228       elsif list
4229         [list]
4230       else
4231         []
4232       end
4233       record.send(:run_association_callbacks, reflection_map[ta], :after_load, list) if after_load_map[ta]
4234       post_process(rec_list, deps) if !rec_list.empty? && !deps.empty?
4235     end
4236   end
4237 end