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