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