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
3727 def initialize(dataset)
3728   opts = dataset.opts
3729   eager_graph = opts[:eager_graph]
3730   @master =  eager_graph[:master]
3731   requirements = eager_graph[:requirements]
3732   reflection_map = @reflection_map = eager_graph[:reflections]
3733   reciprocal_map = @reciprocal_map = eager_graph[:reciprocals]
3734   limit_map = @limit_map = eager_graph[:limits]
3735   @unique = eager_graph[:cartesian_product_number] > 1
3736       
3737   alias_map = @alias_map = {}
3738   type_map = @type_map = {}
3739   after_load_map = @after_load_map = {}
3740   reflection_map.each do |k, v|
3741     alias_map[k] = v[:name]
3742     after_load_map[k] = v[:after_load] if v[:after_load]
3743     type_map[k] = if v.returns_array?
3744       true
3745     elsif (limit_and_offset = limit_map[k]) && !limit_and_offset.last.nil?
3746       :offset
3747     end
3748   end
3749   after_load_map.freeze
3750   alias_map.freeze
3751   type_map.freeze
3752 
3753   # Make dependency map hash out of requirements array for each association.
3754   # This builds a tree of dependencies that will be used for recursion
3755   # to ensure that all parts of the object graph are loaded into the
3756   # appropriate subordinate association.
3757   dependency_map = @dependency_map = {}
3758   # Sort the associations by requirements length, so that
3759   # requirements are added to the dependency hash before their
3760   # dependencies.
3761   requirements.sort_by{|a| a[1].length}.each do |ta, deps|
3762     if deps.empty?
3763       dependency_map[ta] = {}
3764     else
3765       deps = deps.dup
3766       hash = dependency_map[deps.shift]
3767       deps.each do |dep|
3768         hash = hash[dep]
3769       end
3770       hash[ta] = {}
3771     end
3772   end
3773   freezer = lambda do |h|
3774     h.freeze
3775     h.each_value(&freezer)
3776   end
3777   freezer.call(dependency_map)
3778       
3779   datasets = opts[:graph][:table_aliases].to_a.reject{|ta,ds| ds.nil?}
3780   column_aliases = opts[:graph][:column_aliases]
3781   primary_keys = {}
3782   column_maps = {}
3783   models = {}
3784   row_procs = {}
3785   datasets.each do |ta, ds|
3786     models[ta] = ds.model
3787     primary_keys[ta] = []
3788     column_maps[ta] = {}
3789     row_procs[ta] = ds.row_proc
3790   end
3791   column_aliases.each do |col_alias, tc|
3792     ta, column = tc
3793     column_maps[ta][col_alias] = column
3794   end
3795   column_maps.each do |ta, h|
3796     pk = models[ta].primary_key
3797     if pk.is_a?(Array)
3798       primary_keys[ta] = []
3799       h.select{|ca, c| primary_keys[ta] << ca if pk.include?(c)}
3800     else
3801       h.select{|ca, c| primary_keys[ta] = ca if pk == c}
3802     end
3803   end
3804   @column_maps = column_maps.freeze
3805   @primary_keys = primary_keys.freeze
3806   @row_procs = row_procs.freeze
3807 
3808   # For performance, create two special maps for the master table,
3809   # so you can skip a hash lookup.
3810   @master_column_map = column_maps[master]
3811   @master_primary_keys = primary_keys[master]
3812 
3813   # Add a special hash mapping table alias symbols to 5 element arrays that just
3814   # contain the data in other data structures for that table alias.  This is
3815   # used for performance, to get all values in one hash lookup instead of
3816   # separate hash lookups for each data structure.
3817   ta_map = {}
3818   alias_map.each_key do |ta|
3819     ta_map[ta] = [row_procs[ta], alias_map[ta], type_map[ta], reciprocal_map[ta]].freeze
3820   end
3821   @ta_map = ta_map.freeze
3822   freeze
3823 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
3827 def load(hashes)
3828   # This mapping is used to make sure that duplicate entries in the
3829   # result set are mapped to a single record.  For example, using a
3830   # single one_to_many association with 10 associated records,
3831   # the main object column values appear in the object graph 10 times.
3832   # We map by primary key, if available, or by the object's entire values,
3833   # if not. The mapping must be per table, so create sub maps for each table
3834   # alias.
3835   @records_map = records_map = {}
3836   alias_map.keys.each{|ta| records_map[ta] = {}}
3837 
3838   master = master()
3839       
3840   # Assign to local variables for speed increase
3841   rp = row_procs[master]
3842   rm = records_map[master] = {}
3843   dm = dependency_map
3844 
3845   records_map.freeze
3846 
3847   # This will hold the final record set that we will be replacing the object graph with.
3848   records = []
3849 
3850   hashes.each do |h|
3851     unless key = master_pk(h)
3852       key = hkey(master_hfor(h))
3853     end
3854     unless primary_record = rm[key]
3855       primary_record = rm[key] = rp.call(master_hfor(h))
3856       # Only add it to the list of records to return if it is a new record
3857       records.push(primary_record)
3858     end
3859     # Build all associations for the current object and it's dependencies
3860     _load(dm, primary_record, h)
3861   end
3862       
3863   # Remove duplicate records from all associations if this graph could possibly be a cartesian product
3864   # Run after_load procs if there are any
3865   post_process(records, dm) if @unique || !after_load_map.empty? || !limit_map.empty?
3866 
3867   records_map.each_value(&:freeze)
3868   freeze
3869 
3870   records
3871 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
3876 def _load(dependency_map, current, h)
3877   dependency_map.each do |ta, deps|
3878     unless key = pk(ta, h)
3879       ta_h = hfor(ta, h)
3880       unless ta_h.values.any?
3881         assoc_name = alias_map[ta]
3882         unless (assoc = current.associations).has_key?(assoc_name)
3883           assoc[assoc_name] = type_map[ta] ? [] : nil
3884         end
3885         next
3886       end
3887       key = hkey(ta_h)
3888     end
3889     rp, assoc_name, tm, rcm = @ta_map[ta]
3890     rm = records_map[ta]
3891 
3892     # Check type map for all dependencies, and use a unique
3893     # object if any are dependencies for multiple objects,
3894     # to prevent duplicate objects from showing up in the case
3895     # the normal duplicate removal code is not being used.
3896     if !@unique && !deps.empty? && deps.any?{|dep_key,_| @ta_map[dep_key][2]}
3897       key = [current.object_id, key]
3898     end
3899 
3900     unless rec = rm[key]
3901       rec = rm[key] = rp.call(hfor(ta, h))
3902     end
3903 
3904     if tm
3905       unless (assoc = current.associations).has_key?(assoc_name)
3906         assoc[assoc_name] = []
3907       end
3908       assoc[assoc_name].push(rec) 
3909       rec.associations[rcm] = current if rcm
3910     else
3911       current.associations[assoc_name] ||= rec
3912     end
3913     # Recurse into dependencies of the current object
3914     _load(deps, rec, h) unless deps.empty?
3915   end
3916 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
3919 def hfor(ta, h)
3920   out = {}
3921   @column_maps[ta].each{|ca, c| out[c] = h[ca]}
3922   out
3923 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
3927 def hkey(h)
3928   h.sort_by{|x| x[0]}
3929 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
3932 def master_hfor(h)
3933   out = {}
3934   @master_column_map.each{|ca, c| out[c] = h[ca]}
3935   out
3936 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
3939 def master_pk(h)
3940   x = @master_primary_keys
3941   if x.is_a?(Array)
3942     unless x == []
3943       x = x.map{|ca| h[ca]}
3944       x if x.all?
3945     end
3946   else
3947     h[x]
3948   end
3949 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
3952 def pk(ta, h)
3953   x = primary_keys[ta]
3954   if x.is_a?(Array)
3955     unless x == []
3956       x = x.map{|ca| h[ca]}
3957       x if x.all?
3958     end
3959   else
3960     h[x]
3961   end
3962 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
3969 def post_process(records, dependency_map)
3970   records.each do |record|
3971     dependency_map.each do |ta, deps|
3972       assoc_name = alias_map[ta]
3973       list = record.public_send(assoc_name)
3974       rec_list = if type_map[ta]
3975         list.uniq!
3976         if lo = limit_map[ta]
3977           limit, offset = lo
3978           offset ||= 0
3979           if type_map[ta] == :offset
3980             [record.associations[assoc_name] = list[offset]]
3981           else
3982             list.replace(list[(offset)..(limit ? (offset)+limit-1 : -1)] || [])
3983           end
3984         else
3985           list
3986         end
3987       elsif list
3988         [list]
3989       else
3990         []
3991       end
3992       record.send(:run_association_callbacks, reflection_map[ta], :after_load, list) if after_load_map[ta]
3993       post_process(rec_list, deps) if !rec_list.empty? && !deps.empty?
3994     end
3995   end
3996 end