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