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]

Hash with table alias symbol keys and callable values used to create model instances 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
3211 def initialize(dataset)
3212   opts = dataset.opts
3213   eager_graph = opts[:eager_graph]
3214   @master =  eager_graph[:master]
3215   requirements = eager_graph[:requirements]
3216   reflection_map = @reflection_map = eager_graph[:reflections]
3217   reciprocal_map = @reciprocal_map = eager_graph[:reciprocals]
3218   limit_map = @limit_map = eager_graph[:limits]
3219   @unique = eager_graph[:cartesian_product_number] > 1
3220       
3221   alias_map = @alias_map = {}
3222   type_map = @type_map = {}
3223   after_load_map = @after_load_map = {}
3224   reflection_map.each do |k, v|
3225     alias_map[k] = v[:name]
3226     after_load_map[k] = v[:after_load] if v[:after_load]
3227     type_map[k] = if v.returns_array?
3228       true
3229     elsif (limit_and_offset = limit_map[k]) && !limit_and_offset.last.nil?
3230       :offset
3231     end
3232   end
3233 
3234   # Make dependency map hash out of requirements array for each association.
3235   # This builds a tree of dependencies that will be used for recursion
3236   # to ensure that all parts of the object graph are loaded into the
3237   # appropriate subordinate association.
3238   @dependency_map = {}
3239   # Sort the associations by requirements length, so that
3240   # requirements are added to the dependency hash before their
3241   # dependencies.
3242   requirements.sort_by{|a| a[1].length}.each do |ta, deps|
3243     if deps.empty?
3244       dependency_map[ta] = {}
3245     else
3246       deps = deps.dup
3247       hash = dependency_map[deps.shift]
3248       deps.each do |dep|
3249         hash = hash[dep]
3250       end
3251       hash[ta] = {}
3252     end
3253   end
3254       
3255   # This mapping is used to make sure that duplicate entries in the
3256   # result set are mapped to a single record.  For example, using a
3257   # single one_to_many association with 10 associated records,
3258   # the main object column values appear in the object graph 10 times.
3259   # We map by primary key, if available, or by the object's entire values,
3260   # if not. The mapping must be per table, so create sub maps for each table
3261   # alias.
3262   records_map = {@master=>{}}
3263   alias_map.keys.each{|ta| records_map[ta] = {}}
3264   @records_map = records_map
3265 
3266   datasets = opts[:graph][:table_aliases].to_a.reject{|ta,ds| ds.nil?}
3267   column_aliases = opts[:graph][:column_aliases]
3268   primary_keys = {}
3269   column_maps = {}
3270   models = {}
3271   row_procs = {}
3272   datasets.each do |ta, ds|
3273     models[ta] = ds.model
3274     primary_keys[ta] = []
3275     column_maps[ta] = {}
3276     row_procs[ta] = ds.row_proc
3277   end
3278   column_aliases.each do |col_alias, tc|
3279     ta, column = tc
3280     column_maps[ta][col_alias] = column
3281   end
3282   column_maps.each do |ta, h|
3283     pk = models[ta].primary_key
3284     if pk.is_a?(Array)
3285       primary_keys[ta] = []
3286       h.select{|ca, c| primary_keys[ta] << ca if pk.include?(c)}
3287     else
3288       h.select{|ca, c| primary_keys[ta] = ca if pk == c}
3289     end
3290   end
3291   @column_maps = column_maps
3292   @primary_keys = primary_keys
3293   @row_procs = row_procs
3294 
3295   # For performance, create two special maps for the master table,
3296   # so you can skip a hash lookup.
3297   @master_column_map = column_maps[master]
3298   @master_primary_keys = primary_keys[master]
3299 
3300   # Add a special hash mapping table alias symbols to 5 element arrays that just
3301   # contain the data in other data structures for that table alias.  This is
3302   # used for performance, to get all values in one hash lookup instead of
3303   # separate hash lookups for each data structure.
3304   ta_map = {}
3305   alias_map.keys.each do |ta|
3306     ta_map[ta] = [records_map[ta], row_procs[ta], alias_map[ta], type_map[ta], reciprocal_map[ta]]
3307   end
3308   @ta_map = ta_map
3309 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
3313 def load(hashes)
3314   master = master()
3315       
3316   # Assign to local variables for speed increase
3317   rp = row_procs[master]
3318   rm = records_map[master]
3319   dm = dependency_map
3320 
3321   # This will hold the final record set that we will be replacing the object graph with.
3322   records = []
3323 
3324   hashes.each do |h|
3325     unless key = master_pk(h)
3326       key = hkey(master_hfor(h))
3327     end
3328     unless primary_record = rm[key]
3329       primary_record = rm[key] = rp.call(master_hfor(h))
3330       # Only add it to the list of records to return if it is a new record
3331       records.push(primary_record)
3332     end
3333     # Build all associations for the current object and it's dependencies
3334     _load(dm, primary_record, h)
3335   end
3336       
3337   # Remove duplicate records from all associations if this graph could possibly be a cartesian product
3338   # Run after_load procs if there are any
3339   post_process(records, dm) if @unique || !after_load_map.empty? || !limit_map.empty?
3340 
3341   records
3342 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
3347 def _load(dependency_map, current, h)
3348   dependency_map.each do |ta, deps|
3349     unless key = pk(ta, h)
3350       ta_h = hfor(ta, h)
3351       unless ta_h.values.any?
3352         assoc_name = alias_map[ta]
3353         unless (assoc = current.associations).has_key?(assoc_name)
3354           assoc[assoc_name] = type_map[ta] ? [] : nil
3355         end
3356         next
3357       end
3358       key = hkey(ta_h)
3359     end
3360     rm, rp, assoc_name, tm, rcm = @ta_map[ta]
3361 
3362     # Check type map for all dependencies, and use a unique
3363     # object if any are dependencies for multiple objects,
3364     # to prevent duplicate objects from showing up in the case
3365     # the normal duplicate removal code is not being used.
3366     if !@unique && !deps.empty? && deps.any?{|dep_key,_| @ta_map[dep_key][3]}
3367       key = [current.object_id, key]
3368     end
3369 
3370     unless rec = rm[key]
3371       rec = rm[key] = rp.call(hfor(ta, h))
3372     end
3373 
3374     if tm
3375       unless (assoc = current.associations).has_key?(assoc_name)
3376         assoc[assoc_name] = []
3377       end
3378       assoc[assoc_name].push(rec) 
3379       rec.associations[rcm] = current if rcm
3380     else
3381       current.associations[assoc_name] ||= rec
3382     end
3383     # Recurse into dependencies of the current object
3384     _load(deps, rec, h) unless deps.empty?
3385   end
3386 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
3389 def hfor(ta, h)
3390   out = {}
3391   @column_maps[ta].each{|ca, c| out[c] = h[ca]}
3392   out
3393 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
3397 def hkey(h)
3398   h.sort_by{|x| x[0]}
3399 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
3402 def master_hfor(h)
3403   out = {}
3404   @master_column_map.each{|ca, c| out[c] = h[ca]}
3405   out
3406 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
3409 def master_pk(h)
3410   x = @master_primary_keys
3411   if x.is_a?(Array)
3412     unless x == []
3413       x = x.map{|ca| h[ca]}
3414       x if x.all?
3415     end
3416   else
3417     h[x]
3418   end
3419 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
3422 def pk(ta, h)
3423   x = primary_keys[ta]
3424   if x.is_a?(Array)
3425     unless x == []
3426       x = x.map{|ca| h[ca]}
3427       x if x.all?
3428     end
3429   else
3430     h[x]
3431   end
3432 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
3439 def post_process(records, dependency_map)
3440   records.each do |record|
3441     dependency_map.each do |ta, deps|
3442       assoc_name = alias_map[ta]
3443       list = record.public_send(assoc_name)
3444       rec_list = if type_map[ta]
3445         list.uniq!
3446         if lo = limit_map[ta]
3447           limit, offset = lo
3448           offset ||= 0
3449           if type_map[ta] == :offset
3450             [record.associations[assoc_name] = list[offset]]
3451           else
3452             list.replace(list[(offset)..(limit ? (offset)+limit-1 : -1)] || [])
3453           end
3454         else
3455           list
3456         end
3457       elsif list
3458         [list]
3459       else
3460         []
3461       end
3462       record.send(:run_association_callbacks, reflection_map[ta], :after_load, list) if after_load_map[ta]
3463       post_process(rec_list, deps) if !rec_list.empty? && !deps.empty?
3464     end
3465   end
3466 end