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 3727 def initialize(dataset) 3728 opts = dataset.opts 3729 eager_graph = opts[:eager_graph] 3730 @master = eager_graph[:master] 3731 requirements = eager_graph[:requirements] 3732 reflection_map = @reflection_map = eager_graph[:reflections] 3733 reciprocal_map = @reciprocal_map = eager_graph[:reciprocals] 3734 limit_map = @limit_map = eager_graph[:limits] 3735 @unique = eager_graph[:cartesian_product_number] > 1 3736 3737 alias_map = @alias_map = {} 3738 type_map = @type_map = {} 3739 after_load_map = @after_load_map = {} 3740 reflection_map.each do |k, v| 3741 alias_map[k] = v[:name] 3742 after_load_map[k] = v[:after_load] if v[:after_load] 3743 type_map[k] = if v.returns_array? 3744 true 3745 elsif (limit_and_offset = limit_map[k]) && !limit_and_offset.last.nil? 3746 :offset 3747 end 3748 end 3749 after_load_map.freeze 3750 alias_map.freeze 3751 type_map.freeze 3752 3753 # Make dependency map hash out of requirements array for each association. 3754 # This builds a tree of dependencies that will be used for recursion 3755 # to ensure that all parts of the object graph are loaded into the 3756 # appropriate subordinate association. 3757 dependency_map = @dependency_map = {} 3758 # Sort the associations by requirements length, so that 3759 # requirements are added to the dependency hash before their 3760 # dependencies. 3761 requirements.sort_by{|a| a[1].length}.each do |ta, deps| 3762 if deps.empty? 3763 dependency_map[ta] = {} 3764 else 3765 deps = deps.dup 3766 hash = dependency_map[deps.shift] 3767 deps.each do |dep| 3768 hash = hash[dep] 3769 end 3770 hash[ta] = {} 3771 end 3772 end 3773 freezer = lambda do |h| 3774 h.freeze 3775 h.each_value(&freezer) 3776 end 3777 freezer.call(dependency_map) 3778 3779 datasets = opts[:graph][:table_aliases].to_a.reject{|ta,ds| ds.nil?} 3780 column_aliases = opts[:graph][:column_aliases] 3781 primary_keys = {} 3782 column_maps = {} 3783 models = {} 3784 row_procs = {} 3785 datasets.each do |ta, ds| 3786 models[ta] = ds.model 3787 primary_keys[ta] = [] 3788 column_maps[ta] = {} 3789 row_procs[ta] = ds.row_proc 3790 end 3791 column_aliases.each do |col_alias, tc| 3792 ta, column = tc 3793 column_maps[ta][col_alias] = column 3794 end 3795 column_maps.each do |ta, h| 3796 pk = models[ta].primary_key 3797 if pk.is_a?(Array) 3798 primary_keys[ta] = [] 3799 h.select{|ca, c| primary_keys[ta] << ca if pk.include?(c)} 3800 else 3801 h.select{|ca, c| primary_keys[ta] = ca if pk == c} 3802 end 3803 end 3804 @column_maps = column_maps.freeze 3805 @primary_keys = primary_keys.freeze 3806 @row_procs = row_procs.freeze 3807 3808 # For performance, create two special maps for the master table, 3809 # so you can skip a hash lookup. 3810 @master_column_map = column_maps[master] 3811 @master_primary_keys = primary_keys[master] 3812 3813 # Add a special hash mapping table alias symbols to 5 element arrays that just 3814 # contain the data in other data structures for that table alias. This is 3815 # used for performance, to get all values in one hash lookup instead of 3816 # separate hash lookups for each data structure. 3817 ta_map = {} 3818 alias_map.each_key do |ta| 3819 ta_map[ta] = [row_procs[ta], alias_map[ta], type_map[ta], reciprocal_map[ta]].freeze 3820 end 3821 @ta_map = ta_map.freeze 3822 freeze 3823 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 3827 def load(hashes) 3828 # This mapping is used to make sure that duplicate entries in the 3829 # result set are mapped to a single record. For example, using a 3830 # single one_to_many association with 10 associated records, 3831 # the main object column values appear in the object graph 10 times. 3832 # We map by primary key, if available, or by the object's entire values, 3833 # if not. The mapping must be per table, so create sub maps for each table 3834 # alias. 3835 @records_map = records_map = {} 3836 alias_map.keys.each{|ta| records_map[ta] = {}} 3837 3838 master = master() 3839 3840 # Assign to local variables for speed increase 3841 rp = row_procs[master] 3842 rm = records_map[master] = {} 3843 dm = dependency_map 3844 3845 records_map.freeze 3846 3847 # This will hold the final record set that we will be replacing the object graph with. 3848 records = [] 3849 3850 hashes.each do |h| 3851 unless key = master_pk(h) 3852 key = hkey(master_hfor(h)) 3853 end 3854 unless primary_record = rm[key] 3855 primary_record = rm[key] = rp.call(master_hfor(h)) 3856 # Only add it to the list of records to return if it is a new record 3857 records.push(primary_record) 3858 end 3859 # Build all associations for the current object and it's dependencies 3860 _load(dm, primary_record, h) 3861 end 3862 3863 # Remove duplicate records from all associations if this graph could possibly be a cartesian product 3864 # Run after_load procs if there are any 3865 post_process(records, dm) if @unique || !after_load_map.empty? || !limit_map.empty? 3866 3867 records_map.each_value(&:freeze) 3868 freeze 3869 3870 records 3871 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 3876 def _load(dependency_map, current, h) 3877 dependency_map.each do |ta, deps| 3878 unless key = pk(ta, h) 3879 ta_h = hfor(ta, h) 3880 unless ta_h.values.any? 3881 assoc_name = alias_map[ta] 3882 unless (assoc = current.associations).has_key?(assoc_name) 3883 assoc[assoc_name] = type_map[ta] ? [] : nil 3884 end 3885 next 3886 end 3887 key = hkey(ta_h) 3888 end 3889 rp, assoc_name, tm, rcm = @ta_map[ta] 3890 rm = records_map[ta] 3891 3892 # Check type map for all dependencies, and use a unique 3893 # object if any are dependencies for multiple objects, 3894 # to prevent duplicate objects from showing up in the case 3895 # the normal duplicate removal code is not being used. 3896 if !@unique && !deps.empty? && deps.any?{|dep_key,_| @ta_map[dep_key][2]} 3897 key = [current.object_id, key] 3898 end 3899 3900 unless rec = rm[key] 3901 rec = rm[key] = rp.call(hfor(ta, h)) 3902 end 3903 3904 if tm 3905 unless (assoc = current.associations).has_key?(assoc_name) 3906 assoc[assoc_name] = [] 3907 end 3908 assoc[assoc_name].push(rec) 3909 rec.associations[rcm] = current if rcm 3910 else 3911 current.associations[assoc_name] ||= rec 3912 end 3913 # Recurse into dependencies of the current object 3914 _load(deps, rec, h) unless deps.empty? 3915 end 3916 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 3919 def hfor(ta, h) 3920 out = {} 3921 @column_maps[ta].each{|ca, c| out[c] = h[ca]} 3922 out 3923 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 3927 def hkey(h) 3928 h.sort_by{|x| x[0]} 3929 end
Return the subhash for the master table by parsing the values out of the main hash h
# File lib/sequel/model/associations.rb 3932 def master_hfor(h) 3933 out = {} 3934 @master_column_map.each{|ca, c| out[c] = h[ca]} 3935 out 3936 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 3939 def master_pk(h) 3940 x = @master_primary_keys 3941 if x.is_a?(Array) 3942 unless x == [] 3943 x = x.map{|ca| h[ca]} 3944 x if x.all? 3945 end 3946 else 3947 h[x] 3948 end 3949 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 3952 def pk(ta, h) 3953 x = primary_keys[ta] 3954 if x.is_a?(Array) 3955 unless x == [] 3956 x = x.map{|ca| h[ca]} 3957 x if x.all? 3958 end 3959 else 3960 h[x] 3961 end 3962 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 3969 def post_process(records, dependency_map) 3970 records.each do |record| 3971 dependency_map.each do |ta, deps| 3972 assoc_name = alias_map[ta] 3973 list = record.public_send(assoc_name) 3974 rec_list = if type_map[ta] 3975 list.uniq! 3976 if lo = limit_map[ta] 3977 limit, offset = lo 3978 offset ||= 0 3979 if type_map[ta] == :offset 3980 [record.associations[assoc_name] = list[offset]] 3981 else 3982 list.replace(list[(offset)..(limit ? (offset)+limit-1 : -1)] || []) 3983 end 3984 else 3985 list 3986 end 3987 elsif list 3988 [list] 3989 else 3990 [] 3991 end 3992 record.send(:run_association_callbacks, reflection_map[ta], :after_load, list) if after_load_map[ta] 3993 post_process(rec_list, deps) if !rec_list.empty? && !deps.empty? 3994 end 3995 end 3996 end