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