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 3574 def initialize(dataset) 3575 opts = dataset.opts 3576 eager_graph = opts[:eager_graph] 3577 @master = eager_graph[:master] 3578 requirements = eager_graph[:requirements] 3579 reflection_map = @reflection_map = eager_graph[:reflections] 3580 reciprocal_map = @reciprocal_map = eager_graph[:reciprocals] 3581 limit_map = @limit_map = eager_graph[:limits] 3582 @unique = eager_graph[:cartesian_product_number] > 1 3583 3584 alias_map = @alias_map = {} 3585 type_map = @type_map = {} 3586 after_load_map = @after_load_map = {} 3587 reflection_map.each do |k, v| 3588 alias_map[k] = v[:name] 3589 after_load_map[k] = v[:after_load] if v[:after_load] 3590 type_map[k] = if v.returns_array? 3591 true 3592 elsif (limit_and_offset = limit_map[k]) && !limit_and_offset.last.nil? 3593 :offset 3594 end 3595 end 3596 after_load_map.freeze 3597 alias_map.freeze 3598 type_map.freeze 3599 3600 # Make dependency map hash out of requirements array for each association. 3601 # This builds a tree of dependencies that will be used for recursion 3602 # to ensure that all parts of the object graph are loaded into the 3603 # appropriate subordinate association. 3604 dependency_map = @dependency_map = {} 3605 # Sort the associations by requirements length, so that 3606 # requirements are added to the dependency hash before their 3607 # dependencies. 3608 requirements.sort_by{|a| a[1].length}.each do |ta, deps| 3609 if deps.empty? 3610 dependency_map[ta] = {} 3611 else 3612 deps = deps.dup 3613 hash = dependency_map[deps.shift] 3614 deps.each do |dep| 3615 hash = hash[dep] 3616 end 3617 hash[ta] = {} 3618 end 3619 end 3620 freezer = lambda do |h| 3621 h.freeze 3622 h.each_value(&freezer) 3623 end 3624 freezer.call(dependency_map) 3625 3626 datasets = opts[:graph][:table_aliases].to_a.reject{|ta,ds| ds.nil?} 3627 column_aliases = opts[:graph][:column_aliases] 3628 primary_keys = {} 3629 column_maps = {} 3630 models = {} 3631 row_procs = {} 3632 datasets.each do |ta, ds| 3633 models[ta] = ds.model 3634 primary_keys[ta] = [] 3635 column_maps[ta] = {} 3636 row_procs[ta] = ds.row_proc 3637 end 3638 column_aliases.each do |col_alias, tc| 3639 ta, column = tc 3640 column_maps[ta][col_alias] = column 3641 end 3642 column_maps.each do |ta, h| 3643 pk = models[ta].primary_key 3644 if pk.is_a?(Array) 3645 primary_keys[ta] = [] 3646 h.select{|ca, c| primary_keys[ta] << ca if pk.include?(c)} 3647 else 3648 h.select{|ca, c| primary_keys[ta] = ca if pk == c} 3649 end 3650 end 3651 @column_maps = column_maps.freeze 3652 @primary_keys = primary_keys.freeze 3653 @row_procs = row_procs.freeze 3654 3655 # For performance, create two special maps for the master table, 3656 # so you can skip a hash lookup. 3657 @master_column_map = column_maps[master] 3658 @master_primary_keys = primary_keys[master] 3659 3660 # Add a special hash mapping table alias symbols to 5 element arrays that just 3661 # contain the data in other data structures for that table alias. This is 3662 # used for performance, to get all values in one hash lookup instead of 3663 # separate hash lookups for each data structure. 3664 ta_map = {} 3665 alias_map.each_key do |ta| 3666 ta_map[ta] = [row_procs[ta], alias_map[ta], type_map[ta], reciprocal_map[ta]].freeze 3667 end 3668 @ta_map = ta_map.freeze 3669 freeze 3670 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 3674 def load(hashes) 3675 # This mapping is used to make sure that duplicate entries in the 3676 # result set are mapped to a single record. For example, using a 3677 # single one_to_many association with 10 associated records, 3678 # the main object column values appear in the object graph 10 times. 3679 # We map by primary key, if available, or by the object's entire values, 3680 # if not. The mapping must be per table, so create sub maps for each table 3681 # alias. 3682 @records_map = records_map = {} 3683 alias_map.keys.each{|ta| records_map[ta] = {}} 3684 3685 master = master() 3686 3687 # Assign to local variables for speed increase 3688 rp = row_procs[master] 3689 rm = records_map[master] = {} 3690 dm = dependency_map 3691 3692 records_map.freeze 3693 3694 # This will hold the final record set that we will be replacing the object graph with. 3695 records = [] 3696 3697 hashes.each do |h| 3698 unless key = master_pk(h) 3699 key = hkey(master_hfor(h)) 3700 end 3701 unless primary_record = rm[key] 3702 primary_record = rm[key] = rp.call(master_hfor(h)) 3703 # Only add it to the list of records to return if it is a new record 3704 records.push(primary_record) 3705 end 3706 # Build all associations for the current object and it's dependencies 3707 _load(dm, primary_record, h) 3708 end 3709 3710 # Remove duplicate records from all associations if this graph could possibly be a cartesian product 3711 # Run after_load procs if there are any 3712 post_process(records, dm) if @unique || !after_load_map.empty? || !limit_map.empty? 3713 3714 records_map.each_value(&:freeze) 3715 freeze 3716 3717 records 3718 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 3723 def _load(dependency_map, current, h) 3724 dependency_map.each do |ta, deps| 3725 unless key = pk(ta, h) 3726 ta_h = hfor(ta, h) 3727 unless ta_h.values.any? 3728 assoc_name = alias_map[ta] 3729 unless (assoc = current.associations).has_key?(assoc_name) 3730 assoc[assoc_name] = type_map[ta] ? [] : nil 3731 end 3732 next 3733 end 3734 key = hkey(ta_h) 3735 end 3736 rp, assoc_name, tm, rcm = @ta_map[ta] 3737 rm = records_map[ta] 3738 3739 # Check type map for all dependencies, and use a unique 3740 # object if any are dependencies for multiple objects, 3741 # to prevent duplicate objects from showing up in the case 3742 # the normal duplicate removal code is not being used. 3743 if !@unique && !deps.empty? && deps.any?{|dep_key,_| @ta_map[dep_key][2]} 3744 key = [current.object_id, key] 3745 end 3746 3747 unless rec = rm[key] 3748 rec = rm[key] = rp.call(hfor(ta, h)) 3749 end 3750 3751 if tm 3752 unless (assoc = current.associations).has_key?(assoc_name) 3753 assoc[assoc_name] = [] 3754 end 3755 assoc[assoc_name].push(rec) 3756 rec.associations[rcm] = current if rcm 3757 else 3758 current.associations[assoc_name] ||= rec 3759 end 3760 # Recurse into dependencies of the current object 3761 _load(deps, rec, h) unless deps.empty? 3762 end 3763 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 3766 def hfor(ta, h) 3767 out = {} 3768 @column_maps[ta].each{|ca, c| out[c] = h[ca]} 3769 out 3770 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 3774 def hkey(h) 3775 h.sort_by{|x| x[0]} 3776 end
Return the subhash for the master table by parsing the values out of the main hash h
# File lib/sequel/model/associations.rb 3779 def master_hfor(h) 3780 out = {} 3781 @master_column_map.each{|ca, c| out[c] = h[ca]} 3782 out 3783 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 3786 def master_pk(h) 3787 x = @master_primary_keys 3788 if x.is_a?(Array) 3789 unless x == [] 3790 x = x.map{|ca| h[ca]} 3791 x if x.all? 3792 end 3793 else 3794 h[x] 3795 end 3796 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 3799 def pk(ta, h) 3800 x = primary_keys[ta] 3801 if x.is_a?(Array) 3802 unless x == [] 3803 x = x.map{|ca| h[ca]} 3804 x if x.all? 3805 end 3806 else 3807 h[x] 3808 end 3809 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 3816 def post_process(records, dependency_map) 3817 records.each do |record| 3818 dependency_map.each do |ta, deps| 3819 assoc_name = alias_map[ta] 3820 list = record.public_send(assoc_name) 3821 rec_list = if type_map[ta] 3822 list.uniq! 3823 if lo = limit_map[ta] 3824 limit, offset = lo 3825 offset ||= 0 3826 if type_map[ta] == :offset 3827 [record.associations[assoc_name] = list[offset]] 3828 else 3829 list.replace(list[(offset)..(limit ? (offset)+limit-1 : -1)] || []) 3830 end 3831 else 3832 list 3833 end 3834 elsif list 3835 [list] 3836 else 3837 [] 3838 end 3839 record.send(:run_association_callbacks, reflection_map[ta], :after_load, list) if after_load_map[ta] 3840 post_process(rec_list, deps) if !rec_list.empty? && !deps.empty? 3841 end 3842 end 3843 end