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
Hash with table alias symbol keys and callable values used to create model instances 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, line 3182 def initialize(dataset) opts = dataset.opts eager_graph = opts[:eager_graph] @master = eager_graph[:master] requirements = eager_graph[:requirements] reflection_map = @reflection_map = eager_graph[:reflections] reciprocal_map = @reciprocal_map = eager_graph[:reciprocals] limit_map = @limit_map = eager_graph[:limits] @unique = eager_graph[:cartesian_product_number] > 1 alias_map = @alias_map = {} type_map = @type_map = {} after_load_map = @after_load_map = {} reflection_map.each do |k, v| alias_map[k] = v[:name] after_load_map[k] = v[:after_load] unless v[:after_load].empty? type_map[k] = if v.returns_array? true elsif (limit_and_offset = limit_map[k]) && !limit_and_offset.last.nil? :offset end end # Make dependency map hash out of requirements array for each association. # This builds a tree of dependencies that will be used for recursion # to ensure that all parts of the object graph are loaded into the # appropriate subordinate association. @dependency_map = {} # Sort the associations by requirements length, so that # requirements are added to the dependency hash before their # dependencies. requirements.sort_by{|a| a[1].length}.each do |ta, deps| if deps.empty? dependency_map[ta] = {} else deps = deps.dup hash = dependency_map[deps.shift] deps.each do |dep| hash = hash[dep] end hash[ta] = {} end end # This mapping is used to make sure that duplicate entries in the # result set are mapped to a single record. For example, using a # single one_to_many association with 10 associated records, # the main object column values appear in the object graph 10 times. # We map by primary key, if available, or by the object's entire values, # if not. The mapping must be per table, so create sub maps for each table # alias. records_map = {@master=>{}} alias_map.keys.each{|ta| records_map[ta] = {}} @records_map = records_map datasets = opts[:graph][:table_aliases].to_a.reject{|ta,ds| ds.nil?} column_aliases = opts[:graph_aliases] || opts[:graph][:column_aliases] primary_keys = {} column_maps = {} models = {} row_procs = {} datasets.each do |ta, ds| models[ta] = ds.model primary_keys[ta] = [] column_maps[ta] = {} row_procs[ta] = ds.row_proc end column_aliases.each do |col_alias, tc| ta, column = tc column_maps[ta][col_alias] = column end column_maps.each do |ta, h| pk = models[ta].primary_key if pk.is_a?(Array) primary_keys[ta] = [] h.select{|ca, c| primary_keys[ta] << ca if pk.include?(c)} else h.select{|ca, c| primary_keys[ta] = ca if pk == c} end end @column_maps = column_maps @primary_keys = primary_keys @row_procs = row_procs # For performance, create two special maps for the master table, # so you can skip a hash lookup. @master_column_map = column_maps[master] @master_primary_keys = primary_keys[master] # Add a special hash mapping table alias symbols to 5 element arrays that just # contain the data in other data structures for that table alias. This is # used for performance, to get all values in one hash lookup instead of # separate hash lookups for each data structure. ta_map = {} alias_map.keys.each do |ta| ta_map[ta] = [records_map[ta], row_procs[ta], alias_map[ta], type_map[ta], reciprocal_map[ta]] end @ta_map = ta_map 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, line 3284 def load(hashes) master = master() # Assign to local variables for speed increase rp = row_procs[master] rm = records_map[master] dm = dependency_map # This will hold the final record set that we will be replacing the object graph with. records = [] hashes.each do |h| unless key = master_pk(h) key = hkey(master_hfor(h)) end unless primary_record = rm[key] primary_record = rm[key] = rp.call(master_hfor(h)) # Only add it to the list of records to return if it is a new record records.push(primary_record) end # Build all associations for the current object and it's dependencies _load(dm, primary_record, h) end # Remove duplicate records from all associations if this graph could possibly be a cartesian product # Run after_load procs if there are any post_process(records, dm) if @unique || !after_load_map.empty? || !limit_map.empty? records 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, line 3318 def _load(dependency_map, current, h) dependency_map.each do |ta, deps| unless key = pk(ta, h) ta_h = hfor(ta, h) unless ta_h.values.any? assoc_name = alias_map[ta] unless (assoc = current.associations).has_key?(assoc_name) assoc[assoc_name] = type_map[ta] ? [] : nil end next end key = hkey(ta_h) end rm, rp, assoc_name, tm, rcm = @ta_map[ta] unless rec = rm[key] rec = rm[key] = rp.call(hfor(ta, h)) end if tm unless (assoc = current.associations).has_key?(assoc_name) assoc[assoc_name] = [] end assoc[assoc_name].push(rec) rec.associations[rcm] = current if rcm else current.associations[assoc_name] ||= rec end # Recurse into dependencies of the current object _load(deps, rec, h) unless deps.empty? end 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, line 3351 def hfor(ta, h) out = {} @column_maps[ta].each{|ca, c| out[c] = h[ca]} out 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, line 3359 def hkey(h) h.sort_by{|x| x[0].to_s} end
Return the subhash for the master table by parsing the values out of the
main hash h
# File lib/sequel/model/associations.rb, line 3364 def master_hfor(h) out = {} @master_column_map.each{|ca, c| out[c] = h[ca]} out 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, line 3371 def master_pk(h) x = @master_primary_keys if x.is_a?(Array) unless x == [] x = x.map{|ca| h[ca]} x if x.all? end else h[x] end 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, line 3384 def pk(ta, h) x = primary_keys[ta] if x.is_a?(Array) unless x == [] x = x.map{|ca| h[ca]} x if x.all? end else h[x] end 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, line 3401 def post_process(records, dependency_map) records.each do |record| dependency_map.each do |ta, deps| assoc_name = alias_map[ta] list = record.send(assoc_name) rec_list = if type_map[ta] list.uniq! if lo = limit_map[ta] limit, offset = lo offset ||= 0 if type_map[ta] == :offset [record.associations[assoc_name] = list[offset]] else list.replace(list[(offset)..(limit ? (offset)+limit-1 : -1)] || []) end else list end elsif list [list] else [] end record.send(:run_association_callbacks, reflection_map[ta], :after_load, list) if after_load_map[ta] post_process(rec_list, deps) if !rec_list.empty? && !deps.empty? end end end