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
Hash with table alias symbol keys and after_load hook values
Hash with table alias symbol keys and association name values
Hash with table alias symbol keys and subhash values mapping column_alias symbols to the symbol of the real name of the column
Recursive hash with table alias symbol keys mapping to hashes with dependent table alias symbol keys.
Hash with table alias symbol keys and [limit, offset] values
The table alias symbol for the primary model
Hash with table alias symbol keys and primary key symbol values (or arrays of primary key symbols for composite key tables)
Hash with table alias symbol keys and reciprocal association symbol values, used for setting reciprocals for one_to_many associations.
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.
Hash with table alias symbol keys and AssociationReflection values
Hash with table alias symbol keys and callable values used to create model instances
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
Source
# File lib/sequel/model/associations.rb 3968 def initialize(dataset) 3969 opts = dataset.opts 3970 eager_graph = opts[:eager_graph] 3971 @master = eager_graph[:master] 3972 requirements = eager_graph[:requirements] 3973 reflection_map = @reflection_map = eager_graph[:reflections] 3974 reciprocal_map = @reciprocal_map = eager_graph[:reciprocals] 3975 limit_map = @limit_map = eager_graph[:limits] 3976 @unique = eager_graph[:cartesian_product_number] > 1 3977 3978 alias_map = @alias_map = {} 3979 type_map = @type_map = {} 3980 after_load_map = @after_load_map = {} 3981 reflection_map.each do |k, v| 3982 alias_map[k] = v[:name] 3983 after_load_map[k] = v[:after_load] if v[:after_load] 3984 type_map[k] = if v.returns_array? 3985 true 3986 elsif (limit_and_offset = limit_map[k]) && !limit_and_offset.last.nil? 3987 :offset 3988 end 3989 end 3990 after_load_map.freeze 3991 alias_map.freeze 3992 type_map.freeze 3993 3994 # Make dependency map hash out of requirements array for each association. 3995 # This builds a tree of dependencies that will be used for recursion 3996 # to ensure that all parts of the object graph are loaded into the 3997 # appropriate subordinate association. 3998 dependency_map = @dependency_map = {} 3999 # Sort the associations by requirements length, so that 4000 # requirements are added to the dependency hash before their 4001 # dependencies. 4002 requirements.sort_by{|a| a[1].length}.each do |ta, deps| 4003 if deps.empty? 4004 dependency_map[ta] = {} 4005 else 4006 deps = deps.dup 4007 hash = dependency_map[deps.shift] 4008 deps.each do |dep| 4009 hash = hash[dep] 4010 end 4011 hash[ta] = {} 4012 end 4013 end 4014 freezer = lambda do |h| 4015 h.freeze 4016 h.each_value(&freezer) 4017 end 4018 freezer.call(dependency_map) 4019 4020 datasets = opts[:graph][:table_aliases].to_a.reject{|ta,ds| ds.nil?} 4021 column_aliases = opts[:graph][:column_aliases] 4022 primary_keys = {} 4023 column_maps = {} 4024 models = {} 4025 row_procs = {} 4026 datasets.each do |ta, ds| 4027 models[ta] = ds.model 4028 primary_keys[ta] = [] 4029 column_maps[ta] = {} 4030 row_procs[ta] = ds.row_proc 4031 end 4032 column_aliases.each do |col_alias, tc| 4033 ta, column = tc 4034 column_maps[ta][col_alias] = column 4035 end 4036 column_maps.each do |ta, h| 4037 pk = models[ta].primary_key 4038 if pk.is_a?(Array) 4039 primary_keys[ta] = [] 4040 h.select{|ca, c| primary_keys[ta] << ca if pk.include?(c)} 4041 else 4042 h.select{|ca, c| primary_keys[ta] = ca if pk == c} 4043 end 4044 end 4045 @column_maps = column_maps.freeze 4046 @primary_keys = primary_keys.freeze 4047 @row_procs = row_procs.freeze 4048 4049 # For performance, create two special maps for the master table, 4050 # so you can skip a hash lookup. 4051 @master_column_map = column_maps[master] 4052 @master_primary_keys = primary_keys[master] 4053 4054 # Add a special hash mapping table alias symbols to 5 element arrays that just 4055 # contain the data in other data structures for that table alias. This is 4056 # used for performance, to get all values in one hash lookup instead of 4057 # separate hash lookups for each data structure. 4058 ta_map = {} 4059 alias_map.each_key do |ta| 4060 ta_map[ta] = [row_procs[ta], alias_map[ta], type_map[ta], reciprocal_map[ta]].freeze 4061 end 4062 @ta_map = ta_map.freeze 4063 freeze 4064 end
Initialize all of the data structures used during loading.
Public Instance Methods
Source
# File lib/sequel/model/associations.rb 4068 def load(hashes) 4069 # This mapping is used to make sure that duplicate entries in the 4070 # result set are mapped to a single record. For example, using a 4071 # single one_to_many association with 10 associated records, 4072 # the main object column values appear in the object graph 10 times. 4073 # We map by primary key, if available, or by the object's entire values, 4074 # if not. The mapping must be per table, so create sub maps for each table 4075 # alias. 4076 @records_map = records_map = {} 4077 alias_map.keys.each{|ta| records_map[ta] = {}} 4078 4079 master = master() 4080 4081 # Assign to local variables for speed increase 4082 rp = row_procs[master] 4083 rm = records_map[master] = {} 4084 dm = dependency_map 4085 4086 records_map.freeze 4087 4088 # This will hold the final record set that we will be replacing the object graph with. 4089 records = [] 4090 4091 hashes.each do |h| 4092 unless key = master_pk(h) 4093 key = hkey(master_hfor(h)) 4094 end 4095 unless primary_record = rm[key] 4096 primary_record = rm[key] = rp.call(master_hfor(h)) 4097 # Only add it to the list of records to return if it is a new record 4098 records.push(primary_record) 4099 end 4100 # Build all associations for the current object and it's dependencies 4101 _load(dm, primary_record, h) 4102 end 4103 4104 # Remove duplicate records from all associations if this graph could possibly be a cartesian product 4105 # Run after_load procs if there are any 4106 post_process(records, dm) if @unique || !after_load_map.empty? || !limit_map.empty? 4107 4108 records_map.each_value(&:freeze) 4109 freeze 4110 4111 records 4112 end
Return an array of primary model instances with the associations cache prepopulated for all model objects (both primary and associated).
Private Instance Methods
Source
# File lib/sequel/model/associations.rb 4117 def _load(dependency_map, current, h) 4118 dependency_map.each do |ta, deps| 4119 unless key = pk(ta, h) 4120 ta_h = hfor(ta, h) 4121 unless ta_h.values.any? 4122 assoc_name = alias_map[ta] 4123 unless (assoc = current.associations).has_key?(assoc_name) 4124 assoc[assoc_name] = type_map[ta] ? [] : nil 4125 end 4126 next 4127 end 4128 key = hkey(ta_h) 4129 end 4130 rp, assoc_name, tm, rcm = @ta_map[ta] 4131 rm = records_map[ta] 4132 4133 # Check type map for all dependencies, and use a unique 4134 # object if any are dependencies for multiple objects, 4135 # to prevent duplicate objects from showing up in the case 4136 # the normal duplicate removal code is not being used. 4137 if !@unique && !deps.empty? && deps.any?{|dep_key,_| @ta_map[dep_key][2]} 4138 key = [current.object_id, key] 4139 end 4140 4141 unless rec = rm[key] 4142 rec = rm[key] = rp.call(hfor(ta, h)) 4143 end 4144 4145 if tm 4146 unless (assoc = current.associations).has_key?(assoc_name) 4147 assoc[assoc_name] = [] 4148 end 4149 assoc[assoc_name].push(rec) 4150 rec.associations[rcm] = current if rcm 4151 else 4152 current.associations[assoc_name] ||= rec 4153 end 4154 # Recurse into dependencies of the current object 4155 _load(deps, rec, h) unless deps.empty? 4156 end 4157 end
Recursive method that creates associated model objects and associates them to the current model object.
Source
# File lib/sequel/model/associations.rb 4160 def hfor(ta, h) 4161 out = {} 4162 @column_maps[ta].each{|ca, c| out[c] = h[ca]} 4163 out 4164 end
Return the subhash for the specific table alias ta by parsing the values out of the main hash h
Source
# File lib/sequel/model/associations.rb 4168 def hkey(h) 4169 h.sort_by{|x| x[0]} 4170 end
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.
Source
# File lib/sequel/model/associations.rb 4173 def master_hfor(h) 4174 out = {} 4175 @master_column_map.each{|ca, c| out[c] = h[ca]} 4176 out 4177 end
Return the subhash for the master table by parsing the values out of the main hash h
Source
# File lib/sequel/model/associations.rb 4180 def master_pk(h) 4181 x = @master_primary_keys 4182 if x.is_a?(Array) 4183 unless x == [] 4184 x = x.map{|ca| h[ca]} 4185 x if x.all? 4186 end 4187 else 4188 h[x] 4189 end 4190 end
Return a primary key value for the master table by parsing it out of the main hash h.
Source
# File lib/sequel/model/associations.rb 4193 def pk(ta, h) 4194 x = primary_keys[ta] 4195 if x.is_a?(Array) 4196 unless x == [] 4197 x = x.map{|ca| h[ca]} 4198 x if x.all? 4199 end 4200 else 4201 h[x] 4202 end 4203 end
Return a primary key value for the given table alias by parsing it out of the main hash h.
Source
# File lib/sequel/model/associations.rb 4210 def post_process(records, dependency_map) 4211 records.each do |record| 4212 dependency_map.each do |ta, deps| 4213 assoc_name = alias_map[ta] 4214 list = record.public_send(assoc_name) 4215 rec_list = if type_map[ta] 4216 list.uniq! 4217 if lo = limit_map[ta] 4218 limit, offset = lo 4219 offset ||= 0 4220 if type_map[ta] == :offset 4221 [record.associations[assoc_name] = list[offset]] 4222 else 4223 list.replace(list[(offset)..(limit ? (offset)+limit-1 : -1)] || []) 4224 end 4225 else 4226 list 4227 end 4228 elsif list 4229 [list] 4230 else 4231 [] 4232 end 4233 record.send(:run_association_callbacks, reflection_map[ta], :after_load, list) if after_load_map[ta] 4234 post_process(rec_list, deps) if !rec_list.empty? && !deps.empty? 4235 end 4236 end 4237 end
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.