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
Source
# File lib/sequel/model/associations.rb 3967 def initialize(dataset) 3968 opts = dataset.opts 3969 eager_graph = opts[:eager_graph] 3970 @master = eager_graph[:master] 3971 requirements = eager_graph[:requirements] 3972 reflection_map = @reflection_map = eager_graph[:reflections] 3973 reciprocal_map = @reciprocal_map = eager_graph[:reciprocals] 3974 limit_map = @limit_map = eager_graph[:limits] 3975 @unique = eager_graph[:cartesian_product_number] > 1 3976 3977 alias_map = @alias_map = {} 3978 type_map = @type_map = {} 3979 after_load_map = @after_load_map = {} 3980 reflection_map.each do |k, v| 3981 alias_map[k] = v[:name] 3982 after_load_map[k] = v[:after_load] if v[:after_load] 3983 type_map[k] = if v.returns_array? 3984 true 3985 elsif (limit_and_offset = limit_map[k]) && !limit_and_offset.last.nil? 3986 :offset 3987 end 3988 end 3989 after_load_map.freeze 3990 alias_map.freeze 3991 type_map.freeze 3992 3993 # Make dependency map hash out of requirements array for each association. 3994 # This builds a tree of dependencies that will be used for recursion 3995 # to ensure that all parts of the object graph are loaded into the 3996 # appropriate subordinate association. 3997 dependency_map = @dependency_map = {} 3998 # Sort the associations by requirements length, so that 3999 # requirements are added to the dependency hash before their 4000 # dependencies. 4001 requirements.sort_by{|a| a[1].length}.each do |ta, deps| 4002 if deps.empty? 4003 dependency_map[ta] = {} 4004 else 4005 deps = deps.dup 4006 hash = dependency_map[deps.shift] 4007 deps.each do |dep| 4008 hash = hash[dep] 4009 end 4010 hash[ta] = {} 4011 end 4012 end 4013 freezer = lambda do |h| 4014 h.freeze 4015 h.each_value(&freezer) 4016 end 4017 freezer.call(dependency_map) 4018 4019 datasets = opts[:graph][:table_aliases].to_a.reject{|ta,ds| ds.nil?} 4020 column_aliases = opts[:graph][:column_aliases] 4021 primary_keys = {} 4022 column_maps = {} 4023 models = {} 4024 row_procs = {} 4025 datasets.each do |ta, ds| 4026 models[ta] = ds.model 4027 primary_keys[ta] = [] 4028 column_maps[ta] = {} 4029 row_procs[ta] = ds.row_proc 4030 end 4031 column_aliases.each do |col_alias, tc| 4032 ta, column = tc 4033 column_maps[ta][col_alias] = column 4034 end 4035 column_maps.each do |ta, h| 4036 pk = models[ta].primary_key 4037 if pk.is_a?(Array) 4038 primary_keys[ta] = [] 4039 h.select{|ca, c| primary_keys[ta] << ca if pk.include?(c)} 4040 else 4041 h.select{|ca, c| primary_keys[ta] = ca if pk == c} 4042 end 4043 end 4044 @column_maps = column_maps.freeze 4045 @primary_keys = primary_keys.freeze 4046 @row_procs = row_procs.freeze 4047 4048 # For performance, create two special maps for the master table, 4049 # so you can skip a hash lookup. 4050 @master_column_map = column_maps[master] 4051 @master_primary_keys = primary_keys[master] 4052 4053 # Add a special hash mapping table alias symbols to 5 element arrays that just 4054 # contain the data in other data structures for that table alias. This is 4055 # used for performance, to get all values in one hash lookup instead of 4056 # separate hash lookups for each data structure. 4057 ta_map = {} 4058 alias_map.each_key do |ta| 4059 ta_map[ta] = [row_procs[ta], alias_map[ta], type_map[ta], reciprocal_map[ta]].freeze 4060 end 4061 @ta_map = ta_map.freeze 4062 freeze 4063 end
Initialize all of the data structures used during loading.
Public Instance Methods
Source
# File lib/sequel/model/associations.rb 4067 def load(hashes) 4068 # This mapping is used to make sure that duplicate entries in the 4069 # result set are mapped to a single record. For example, using a 4070 # single one_to_many association with 10 associated records, 4071 # the main object column values appear in the object graph 10 times. 4072 # We map by primary key, if available, or by the object's entire values, 4073 # if not. The mapping must be per table, so create sub maps for each table 4074 # alias. 4075 @records_map = records_map = {} 4076 alias_map.keys.each{|ta| records_map[ta] = {}} 4077 4078 master = master() 4079 4080 # Assign to local variables for speed increase 4081 rp = row_procs[master] 4082 rm = records_map[master] = {} 4083 dm = dependency_map 4084 4085 records_map.freeze 4086 4087 # This will hold the final record set that we will be replacing the object graph with. 4088 records = [] 4089 4090 hashes.each do |h| 4091 unless key = master_pk(h) 4092 key = hkey(master_hfor(h)) 4093 end 4094 unless primary_record = rm[key] 4095 primary_record = rm[key] = rp.call(master_hfor(h)) 4096 # Only add it to the list of records to return if it is a new record 4097 records.push(primary_record) 4098 end 4099 # Build all associations for the current object and it's dependencies 4100 _load(dm, primary_record, h) 4101 end 4102 4103 # Remove duplicate records from all associations if this graph could possibly be a cartesian product 4104 # Run after_load procs if there are any 4105 post_process(records, dm) if @unique || !after_load_map.empty? || !limit_map.empty? 4106 4107 records_map.each_value(&:freeze) 4108 freeze 4109 4110 records 4111 end
Return an array of primary model instances with the associations cache prepopulated for all model objects (both primary and associated).
Private Instance Methods
Source
# File lib/sequel/model/associations.rb 4116 def _load(dependency_map, current, h) 4117 dependency_map.each do |ta, deps| 4118 unless key = pk(ta, h) 4119 ta_h = hfor(ta, h) 4120 unless ta_h.values.any? 4121 assoc_name = alias_map[ta] 4122 unless (assoc = current.associations).has_key?(assoc_name) 4123 assoc[assoc_name] = type_map[ta] ? [] : nil 4124 end 4125 next 4126 end 4127 key = hkey(ta_h) 4128 end 4129 rp, assoc_name, tm, rcm = @ta_map[ta] 4130 rm = records_map[ta] 4131 4132 # Check type map for all dependencies, and use a unique 4133 # object if any are dependencies for multiple objects, 4134 # to prevent duplicate objects from showing up in the case 4135 # the normal duplicate removal code is not being used. 4136 if !@unique && !deps.empty? && deps.any?{|dep_key,_| @ta_map[dep_key][2]} 4137 key = [current.object_id, key] 4138 end 4139 4140 unless rec = rm[key] 4141 rec = rm[key] = rp.call(hfor(ta, h)) 4142 end 4143 4144 if tm 4145 unless (assoc = current.associations).has_key?(assoc_name) 4146 assoc[assoc_name] = [] 4147 end 4148 assoc[assoc_name].push(rec) 4149 rec.associations[rcm] = current if rcm 4150 else 4151 current.associations[assoc_name] ||= rec 4152 end 4153 # Recurse into dependencies of the current object 4154 _load(deps, rec, h) unless deps.empty? 4155 end 4156 end
Recursive method that creates associated model objects and associates them to the current model object.
Source
# File lib/sequel/model/associations.rb 4159 def hfor(ta, h) 4160 out = {} 4161 @column_maps[ta].each{|ca, c| out[c] = h[ca]} 4162 out 4163 end
Return the subhash for the specific table alias ta by parsing the values out of the main hash h
Source
# File lib/sequel/model/associations.rb 4167 def hkey(h) 4168 h.sort_by{|x| x[0]} 4169 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.
Source
# File lib/sequel/model/associations.rb 4172 def master_hfor(h) 4173 out = {} 4174 @master_column_map.each{|ca, c| out[c] = h[ca]} 4175 out 4176 end
Return the subhash for the master table by parsing the values out of the main hash h
Source
# File lib/sequel/model/associations.rb 4179 def master_pk(h) 4180 x = @master_primary_keys 4181 if x.is_a?(Array) 4182 unless x == [] 4183 x = x.map{|ca| h[ca]} 4184 x if x.all? 4185 end 4186 else 4187 h[x] 4188 end 4189 end
Return a primary key value for the master table by parsing it out of the main hash h.
Source
# File lib/sequel/model/associations.rb 4192 def pk(ta, h) 4193 x = primary_keys[ta] 4194 if x.is_a?(Array) 4195 unless x == [] 4196 x = x.map{|ca| h[ca]} 4197 x if x.all? 4198 end 4199 else 4200 h[x] 4201 end 4202 end
Return a primary key value for the given table alias by parsing it out of the main hash h.
Source
# File lib/sequel/model/associations.rb 4209 def post_process(records, dependency_map) 4210 records.each do |record| 4211 dependency_map.each do |ta, deps| 4212 assoc_name = alias_map[ta] 4213 list = record.public_send(assoc_name) 4214 rec_list = if type_map[ta] 4215 list.uniq! 4216 if lo = limit_map[ta] 4217 limit, offset = lo 4218 offset ||= 0 4219 if type_map[ta] == :offset 4220 [record.associations[assoc_name] = list[offset]] 4221 else 4222 list.replace(list[(offset)..(limit ? (offset)+limit-1 : -1)] || []) 4223 end 4224 else 4225 list 4226 end 4227 elsif list 4228 [list] 4229 else 4230 [] 4231 end 4232 record.send(:run_association_callbacks, reflection_map[ta], :after_load, list) if after_load_map[ta] 4233 post_process(rec_list, deps) if !rec_list.empty? && !deps.empty? 4234 end 4235 end 4236 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.