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