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