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