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
Initialize all of the data structures used during loading.
# File lib/sequel/model/associations.rb 3488 def initialize(dataset) 3489 opts = dataset.opts 3490 eager_graph = opts[:eager_graph] 3491 @master = eager_graph[:master] 3492 requirements = eager_graph[:requirements] 3493 reflection_map = @reflection_map = eager_graph[:reflections] 3494 reciprocal_map = @reciprocal_map = eager_graph[:reciprocals] 3495 limit_map = @limit_map = eager_graph[:limits] 3496 @unique = eager_graph[:cartesian_product_number] > 1 3497 3498 alias_map = @alias_map = {} 3499 type_map = @type_map = {} 3500 after_load_map = @after_load_map = {} 3501 reflection_map.each do |k, v| 3502 alias_map[k] = v[:name] 3503 after_load_map[k] = v[:after_load] if v[:after_load] 3504 type_map[k] = if v.returns_array? 3505 true 3506 elsif (limit_and_offset = limit_map[k]) && !limit_and_offset.last.nil? 3507 :offset 3508 end 3509 end 3510 after_load_map.freeze 3511 alias_map.freeze 3512 type_map.freeze 3513 3514 # Make dependency map hash out of requirements array for each association. 3515 # This builds a tree of dependencies that will be used for recursion 3516 # to ensure that all parts of the object graph are loaded into the 3517 # appropriate subordinate association. 3518 dependency_map = @dependency_map = {} 3519 # Sort the associations by requirements length, so that 3520 # requirements are added to the dependency hash before their 3521 # dependencies. 3522 requirements.sort_by{|a| a[1].length}.each do |ta, deps| 3523 if deps.empty? 3524 dependency_map[ta] = {} 3525 else 3526 deps = deps.dup 3527 hash = dependency_map[deps.shift] 3528 deps.each do |dep| 3529 hash = hash[dep] 3530 end 3531 hash[ta] = {} 3532 end 3533 end 3534 freezer = lambda do |h| 3535 h.freeze 3536 h.each_value(&freezer) 3537 end 3538 freezer.call(dependency_map) 3539 3540 datasets = opts[:graph][:table_aliases].to_a.reject{|ta,ds| ds.nil?} 3541 column_aliases = opts[:graph][:column_aliases] 3542 primary_keys = {} 3543 column_maps = {} 3544 models = {} 3545 row_procs = {} 3546 datasets.each do |ta, ds| 3547 models[ta] = ds.model 3548 primary_keys[ta] = [] 3549 column_maps[ta] = {} 3550 row_procs[ta] = ds.row_proc 3551 end 3552 column_aliases.each do |col_alias, tc| 3553 ta, column = tc 3554 column_maps[ta][col_alias] = column 3555 end 3556 column_maps.each do |ta, h| 3557 pk = models[ta].primary_key 3558 if pk.is_a?(Array) 3559 primary_keys[ta] = [] 3560 h.select{|ca, c| primary_keys[ta] << ca if pk.include?(c)} 3561 else 3562 h.select{|ca, c| primary_keys[ta] = ca if pk == c} 3563 end 3564 end 3565 @column_maps = column_maps.freeze 3566 @primary_keys = primary_keys.freeze 3567 @row_procs = row_procs.freeze 3568 3569 # For performance, create two special maps for the master table, 3570 # so you can skip a hash lookup. 3571 @master_column_map = column_maps[master] 3572 @master_primary_keys = primary_keys[master] 3573 3574 # Add a special hash mapping table alias symbols to 5 element arrays that just 3575 # contain the data in other data structures for that table alias. This is 3576 # used for performance, to get all values in one hash lookup instead of 3577 # separate hash lookups for each data structure. 3578 ta_map = {} 3579 alias_map.each_key do |ta| 3580 ta_map[ta] = [row_procs[ta], alias_map[ta], type_map[ta], reciprocal_map[ta]].freeze 3581 end 3582 @ta_map = ta_map.freeze 3583 freeze 3584 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 3588 def load(hashes) 3589 # This mapping is used to make sure that duplicate entries in the 3590 # result set are mapped to a single record. For example, using a 3591 # single one_to_many association with 10 associated records, 3592 # the main object column values appear in the object graph 10 times. 3593 # We map by primary key, if available, or by the object's entire values, 3594 # if not. The mapping must be per table, so create sub maps for each table 3595 # alias. 3596 @records_map = records_map = {} 3597 alias_map.keys.each{|ta| records_map[ta] = {}} 3598 3599 master = master() 3600 3601 # Assign to local variables for speed increase 3602 rp = row_procs[master] 3603 rm = records_map[master] = {} 3604 dm = dependency_map 3605 3606 records_map.freeze 3607 3608 # This will hold the final record set that we will be replacing the object graph with. 3609 records = [] 3610 3611 hashes.each do |h| 3612 unless key = master_pk(h) 3613 key = hkey(master_hfor(h)) 3614 end 3615 unless primary_record = rm[key] 3616 primary_record = rm[key] = rp.call(master_hfor(h)) 3617 # Only add it to the list of records to return if it is a new record 3618 records.push(primary_record) 3619 end 3620 # Build all associations for the current object and it's dependencies 3621 _load(dm, primary_record, h) 3622 end 3623 3624 # Remove duplicate records from all associations if this graph could possibly be a cartesian product 3625 # Run after_load procs if there are any 3626 post_process(records, dm) if @unique || !after_load_map.empty? || !limit_map.empty? 3627 3628 records_map.each_value(&:freeze) 3629 freeze 3630 3631 records 3632 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 3637 def _load(dependency_map, current, h) 3638 dependency_map.each do |ta, deps| 3639 unless key = pk(ta, h) 3640 ta_h = hfor(ta, h) 3641 unless ta_h.values.any? 3642 assoc_name = alias_map[ta] 3643 unless (assoc = current.associations).has_key?(assoc_name) 3644 assoc[assoc_name] = type_map[ta] ? [] : nil 3645 end 3646 next 3647 end 3648 key = hkey(ta_h) 3649 end 3650 rp, assoc_name, tm, rcm = @ta_map[ta] 3651 rm = records_map[ta] 3652 3653 # Check type map for all dependencies, and use a unique 3654 # object if any are dependencies for multiple objects, 3655 # to prevent duplicate objects from showing up in the case 3656 # the normal duplicate removal code is not being used. 3657 if !@unique && !deps.empty? && deps.any?{|dep_key,_| @ta_map[dep_key][2]} 3658 key = [current.object_id, key] 3659 end 3660 3661 unless rec = rm[key] 3662 rec = rm[key] = rp.call(hfor(ta, h)) 3663 end 3664 3665 if tm 3666 unless (assoc = current.associations).has_key?(assoc_name) 3667 assoc[assoc_name] = [] 3668 end 3669 assoc[assoc_name].push(rec) 3670 rec.associations[rcm] = current if rcm 3671 else 3672 current.associations[assoc_name] ||= rec 3673 end 3674 # Recurse into dependencies of the current object 3675 _load(deps, rec, h) unless deps.empty? 3676 end 3677 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 3680 def hfor(ta, h) 3681 out = {} 3682 @column_maps[ta].each{|ca, c| out[c] = h[ca]} 3683 out 3684 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 3688 def hkey(h) 3689 h.sort_by{|x| x[0]} 3690 end
Return the subhash for the master table by parsing the values out of the main hash h
# File lib/sequel/model/associations.rb 3693 def master_hfor(h) 3694 out = {} 3695 @master_column_map.each{|ca, c| out[c] = h[ca]} 3696 out 3697 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 3700 def master_pk(h) 3701 x = @master_primary_keys 3702 if x.is_a?(Array) 3703 unless x == [] 3704 x = x.map{|ca| h[ca]} 3705 x if x.all? 3706 end 3707 else 3708 h[x] 3709 end 3710 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 3713 def pk(ta, h) 3714 x = primary_keys[ta] 3715 if x.is_a?(Array) 3716 unless x == [] 3717 x = x.map{|ca| h[ca]} 3718 x if x.all? 3719 end 3720 else 3721 h[x] 3722 end 3723 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 3730 def post_process(records, dependency_map) 3731 records.each do |record| 3732 dependency_map.each do |ta, deps| 3733 assoc_name = alias_map[ta] 3734 list = record.public_send(assoc_name) 3735 rec_list = if type_map[ta] 3736 list.uniq! 3737 if lo = limit_map[ta] 3738 limit, offset = lo 3739 offset ||= 0 3740 if type_map[ta] == :offset 3741 [record.associations[assoc_name] = list[offset]] 3742 else 3743 list.replace(list[(offset)..(limit ? (offset)+limit-1 : -1)] || []) 3744 end 3745 else 3746 list 3747 end 3748 elsif list 3749 [list] 3750 else 3751 [] 3752 end 3753 record.send(:run_association_callbacks, reflection_map[ta], :after_load, list) if after_load_map[ta] 3754 post_process(rec_list, deps) if !rec_list.empty? && !deps.empty? 3755 end 3756 end 3757 end