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