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
Hash
with table alias symbol keys and callable values used to create model instances 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
Initialize all of the data structures used during loading.
# File lib/sequel/model/associations.rb 3211 def initialize(dataset) 3212 opts = dataset.opts 3213 eager_graph = opts[:eager_graph] 3214 @master = eager_graph[:master] 3215 requirements = eager_graph[:requirements] 3216 reflection_map = @reflection_map = eager_graph[:reflections] 3217 reciprocal_map = @reciprocal_map = eager_graph[:reciprocals] 3218 limit_map = @limit_map = eager_graph[:limits] 3219 @unique = eager_graph[:cartesian_product_number] > 1 3220 3221 alias_map = @alias_map = {} 3222 type_map = @type_map = {} 3223 after_load_map = @after_load_map = {} 3224 reflection_map.each do |k, v| 3225 alias_map[k] = v[:name] 3226 after_load_map[k] = v[:after_load] if v[:after_load] 3227 type_map[k] = if v.returns_array? 3228 true 3229 elsif (limit_and_offset = limit_map[k]) && !limit_and_offset.last.nil? 3230 :offset 3231 end 3232 end 3233 3234 # Make dependency map hash out of requirements array for each association. 3235 # This builds a tree of dependencies that will be used for recursion 3236 # to ensure that all parts of the object graph are loaded into the 3237 # appropriate subordinate association. 3238 @dependency_map = {} 3239 # Sort the associations by requirements length, so that 3240 # requirements are added to the dependency hash before their 3241 # dependencies. 3242 requirements.sort_by{|a| a[1].length}.each do |ta, deps| 3243 if deps.empty? 3244 dependency_map[ta] = {} 3245 else 3246 deps = deps.dup 3247 hash = dependency_map[deps.shift] 3248 deps.each do |dep| 3249 hash = hash[dep] 3250 end 3251 hash[ta] = {} 3252 end 3253 end 3254 3255 # This mapping is used to make sure that duplicate entries in the 3256 # result set are mapped to a single record. For example, using a 3257 # single one_to_many association with 10 associated records, 3258 # the main object column values appear in the object graph 10 times. 3259 # We map by primary key, if available, or by the object's entire values, 3260 # if not. The mapping must be per table, so create sub maps for each table 3261 # alias. 3262 records_map = {@master=>{}} 3263 alias_map.keys.each{|ta| records_map[ta] = {}} 3264 @records_map = records_map 3265 3266 datasets = opts[:graph][:table_aliases].to_a.reject{|ta,ds| ds.nil?} 3267 column_aliases = opts[:graph][:column_aliases] 3268 primary_keys = {} 3269 column_maps = {} 3270 models = {} 3271 row_procs = {} 3272 datasets.each do |ta, ds| 3273 models[ta] = ds.model 3274 primary_keys[ta] = [] 3275 column_maps[ta] = {} 3276 row_procs[ta] = ds.row_proc 3277 end 3278 column_aliases.each do |col_alias, tc| 3279 ta, column = tc 3280 column_maps[ta][col_alias] = column 3281 end 3282 column_maps.each do |ta, h| 3283 pk = models[ta].primary_key 3284 if pk.is_a?(Array) 3285 primary_keys[ta] = [] 3286 h.select{|ca, c| primary_keys[ta] << ca if pk.include?(c)} 3287 else 3288 h.select{|ca, c| primary_keys[ta] = ca if pk == c} 3289 end 3290 end 3291 @column_maps = column_maps 3292 @primary_keys = primary_keys 3293 @row_procs = row_procs 3294 3295 # For performance, create two special maps for the master table, 3296 # so you can skip a hash lookup. 3297 @master_column_map = column_maps[master] 3298 @master_primary_keys = primary_keys[master] 3299 3300 # Add a special hash mapping table alias symbols to 5 element arrays that just 3301 # contain the data in other data structures for that table alias. This is 3302 # used for performance, to get all values in one hash lookup instead of 3303 # separate hash lookups for each data structure. 3304 ta_map = {} 3305 alias_map.keys.each do |ta| 3306 ta_map[ta] = [records_map[ta], row_procs[ta], alias_map[ta], type_map[ta], reciprocal_map[ta]] 3307 end 3308 @ta_map = ta_map 3309 end
Public Instance Methods
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 3313 def load(hashes) 3314 master = master() 3315 3316 # Assign to local variables for speed increase 3317 rp = row_procs[master] 3318 rm = records_map[master] 3319 dm = dependency_map 3320 3321 # This will hold the final record set that we will be replacing the object graph with. 3322 records = [] 3323 3324 hashes.each do |h| 3325 unless key = master_pk(h) 3326 key = hkey(master_hfor(h)) 3327 end 3328 unless primary_record = rm[key] 3329 primary_record = rm[key] = rp.call(master_hfor(h)) 3330 # Only add it to the list of records to return if it is a new record 3331 records.push(primary_record) 3332 end 3333 # Build all associations for the current object and it's dependencies 3334 _load(dm, primary_record, h) 3335 end 3336 3337 # Remove duplicate records from all associations if this graph could possibly be a cartesian product 3338 # Run after_load procs if there are any 3339 post_process(records, dm) if @unique || !after_load_map.empty? || !limit_map.empty? 3340 3341 records 3342 end
Private Instance Methods
Recursive method that creates associated model objects and associates them to the current model object.
# File lib/sequel/model/associations.rb 3347 def _load(dependency_map, current, h) 3348 dependency_map.each do |ta, deps| 3349 unless key = pk(ta, h) 3350 ta_h = hfor(ta, h) 3351 unless ta_h.values.any? 3352 assoc_name = alias_map[ta] 3353 unless (assoc = current.associations).has_key?(assoc_name) 3354 assoc[assoc_name] = type_map[ta] ? [] : nil 3355 end 3356 next 3357 end 3358 key = hkey(ta_h) 3359 end 3360 rm, rp, assoc_name, tm, rcm = @ta_map[ta] 3361 3362 # Check type map for all dependencies, and use a unique 3363 # object if any are dependencies for multiple objects, 3364 # to prevent duplicate objects from showing up in the case 3365 # the normal duplicate removal code is not being used. 3366 if !@unique && !deps.empty? && deps.any?{|dep_key,_| @ta_map[dep_key][3]} 3367 key = [current.object_id, key] 3368 end 3369 3370 unless rec = rm[key] 3371 rec = rm[key] = rp.call(hfor(ta, h)) 3372 end 3373 3374 if tm 3375 unless (assoc = current.associations).has_key?(assoc_name) 3376 assoc[assoc_name] = [] 3377 end 3378 assoc[assoc_name].push(rec) 3379 rec.associations[rcm] = current if rcm 3380 else 3381 current.associations[assoc_name] ||= rec 3382 end 3383 # Recurse into dependencies of the current object 3384 _load(deps, rec, h) unless deps.empty? 3385 end 3386 end
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 3389 def hfor(ta, h) 3390 out = {} 3391 @column_maps[ta].each{|ca, c| out[c] = h[ca]} 3392 out 3393 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.
# File lib/sequel/model/associations.rb 3397 def hkey(h) 3398 h.sort_by{|x| x[0]} 3399 end
Return the subhash for the master table by parsing the values out of the main hash h
# File lib/sequel/model/associations.rb 3402 def master_hfor(h) 3403 out = {} 3404 @master_column_map.each{|ca, c| out[c] = h[ca]} 3405 out 3406 end
Return a primary key value for the master table by parsing it out of the main hash h
.
# File lib/sequel/model/associations.rb 3409 def master_pk(h) 3410 x = @master_primary_keys 3411 if x.is_a?(Array) 3412 unless x == [] 3413 x = x.map{|ca| h[ca]} 3414 x if x.all? 3415 end 3416 else 3417 h[x] 3418 end 3419 end
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 3422 def pk(ta, h) 3423 x = primary_keys[ta] 3424 if x.is_a?(Array) 3425 unless x == [] 3426 x = x.map{|ca| h[ca]} 3427 x if x.all? 3428 end 3429 else 3430 h[x] 3431 end 3432 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.
# File lib/sequel/model/associations.rb 3439 def post_process(records, dependency_map) 3440 records.each do |record| 3441 dependency_map.each do |ta, deps| 3442 assoc_name = alias_map[ta] 3443 list = record.public_send(assoc_name) 3444 rec_list = if type_map[ta] 3445 list.uniq! 3446 if lo = limit_map[ta] 3447 limit, offset = lo 3448 offset ||= 0 3449 if type_map[ta] == :offset 3450 [record.associations[assoc_name] = list[offset]] 3451 else 3452 list.replace(list[(offset)..(limit ? (offset)+limit-1 : -1)] || []) 3453 end 3454 else 3455 list 3456 end 3457 elsif list 3458 [list] 3459 else 3460 [] 3461 end 3462 record.send(:run_association_callbacks, reflection_map[ta], :after_load, list) if after_load_map[ta] 3463 post_process(rec_list, deps) if !rec_list.empty? && !deps.empty? 3464 end 3465 end 3466 end