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 3517 def initialize(dataset) 3518 opts = dataset.opts 3519 eager_graph = opts[:eager_graph] 3520 @master = eager_graph[:master] 3521 requirements = eager_graph[:requirements] 3522 reflection_map = @reflection_map = eager_graph[:reflections] 3523 reciprocal_map = @reciprocal_map = eager_graph[:reciprocals] 3524 limit_map = @limit_map = eager_graph[:limits] 3525 @unique = eager_graph[:cartesian_product_number] > 1 3526 3527 alias_map = @alias_map = {} 3528 type_map = @type_map = {} 3529 after_load_map = @after_load_map = {} 3530 reflection_map.each do |k, v| 3531 alias_map[k] = v[:name] 3532 after_load_map[k] = v[:after_load] if v[:after_load] 3533 type_map[k] = if v.returns_array? 3534 true 3535 elsif (limit_and_offset = limit_map[k]) && !limit_and_offset.last.nil? 3536 :offset 3537 end 3538 end 3539 after_load_map.freeze 3540 alias_map.freeze 3541 type_map.freeze 3542 3543 # Make dependency map hash out of requirements array for each association. 3544 # This builds a tree of dependencies that will be used for recursion 3545 # to ensure that all parts of the object graph are loaded into the 3546 # appropriate subordinate association. 3547 dependency_map = @dependency_map = {} 3548 # Sort the associations by requirements length, so that 3549 # requirements are added to the dependency hash before their 3550 # dependencies. 3551 requirements.sort_by{|a| a[1].length}.each do |ta, deps| 3552 if deps.empty? 3553 dependency_map[ta] = {} 3554 else 3555 deps = deps.dup 3556 hash = dependency_map[deps.shift] 3557 deps.each do |dep| 3558 hash = hash[dep] 3559 end 3560 hash[ta] = {} 3561 end 3562 end 3563 freezer = lambda do |h| 3564 h.freeze 3565 h.each_value(&freezer) 3566 end 3567 freezer.call(dependency_map) 3568 3569 datasets = opts[:graph][:table_aliases].to_a.reject{|ta,ds| ds.nil?} 3570 column_aliases = opts[:graph][:column_aliases] 3571 primary_keys = {} 3572 column_maps = {} 3573 models = {} 3574 row_procs = {} 3575 datasets.each do |ta, ds| 3576 models[ta] = ds.model 3577 primary_keys[ta] = [] 3578 column_maps[ta] = {} 3579 row_procs[ta] = ds.row_proc 3580 end 3581 column_aliases.each do |col_alias, tc| 3582 ta, column = tc 3583 column_maps[ta][col_alias] = column 3584 end 3585 column_maps.each do |ta, h| 3586 pk = models[ta].primary_key 3587 if pk.is_a?(Array) 3588 primary_keys[ta] = [] 3589 h.select{|ca, c| primary_keys[ta] << ca if pk.include?(c)} 3590 else 3591 h.select{|ca, c| primary_keys[ta] = ca if pk == c} 3592 end 3593 end 3594 @column_maps = column_maps.freeze 3595 @primary_keys = primary_keys.freeze 3596 @row_procs = row_procs.freeze 3597 3598 # For performance, create two special maps for the master table, 3599 # so you can skip a hash lookup. 3600 @master_column_map = column_maps[master] 3601 @master_primary_keys = primary_keys[master] 3602 3603 # Add a special hash mapping table alias symbols to 5 element arrays that just 3604 # contain the data in other data structures for that table alias. This is 3605 # used for performance, to get all values in one hash lookup instead of 3606 # separate hash lookups for each data structure. 3607 ta_map = {} 3608 alias_map.each_key do |ta| 3609 ta_map[ta] = [row_procs[ta], alias_map[ta], type_map[ta], reciprocal_map[ta]].freeze 3610 end 3611 @ta_map = ta_map.freeze 3612 freeze 3613 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 3617 def load(hashes) 3618 # This mapping is used to make sure that duplicate entries in the 3619 # result set are mapped to a single record. For example, using a 3620 # single one_to_many association with 10 associated records, 3621 # the main object column values appear in the object graph 10 times. 3622 # We map by primary key, if available, or by the object's entire values, 3623 # if not. The mapping must be per table, so create sub maps for each table 3624 # alias. 3625 @records_map = records_map = {} 3626 alias_map.keys.each{|ta| records_map[ta] = {}} 3627 3628 master = master() 3629 3630 # Assign to local variables for speed increase 3631 rp = row_procs[master] 3632 rm = records_map[master] = {} 3633 dm = dependency_map 3634 3635 records_map.freeze 3636 3637 # This will hold the final record set that we will be replacing the object graph with. 3638 records = [] 3639 3640 hashes.each do |h| 3641 unless key = master_pk(h) 3642 key = hkey(master_hfor(h)) 3643 end 3644 unless primary_record = rm[key] 3645 primary_record = rm[key] = rp.call(master_hfor(h)) 3646 # Only add it to the list of records to return if it is a new record 3647 records.push(primary_record) 3648 end 3649 # Build all associations for the current object and it's dependencies 3650 _load(dm, primary_record, h) 3651 end 3652 3653 # Remove duplicate records from all associations if this graph could possibly be a cartesian product 3654 # Run after_load procs if there are any 3655 post_process(records, dm) if @unique || !after_load_map.empty? || !limit_map.empty? 3656 3657 records_map.each_value(&:freeze) 3658 freeze 3659 3660 records 3661 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 3666 def _load(dependency_map, current, h) 3667 dependency_map.each do |ta, deps| 3668 unless key = pk(ta, h) 3669 ta_h = hfor(ta, h) 3670 unless ta_h.values.any? 3671 assoc_name = alias_map[ta] 3672 unless (assoc = current.associations).has_key?(assoc_name) 3673 assoc[assoc_name] = type_map[ta] ? [] : nil 3674 end 3675 next 3676 end 3677 key = hkey(ta_h) 3678 end 3679 rp, assoc_name, tm, rcm = @ta_map[ta] 3680 rm = records_map[ta] 3681 3682 # Check type map for all dependencies, and use a unique 3683 # object if any are dependencies for multiple objects, 3684 # to prevent duplicate objects from showing up in the case 3685 # the normal duplicate removal code is not being used. 3686 if !@unique && !deps.empty? && deps.any?{|dep_key,_| @ta_map[dep_key][2]} 3687 key = [current.object_id, key] 3688 end 3689 3690 unless rec = rm[key] 3691 rec = rm[key] = rp.call(hfor(ta, h)) 3692 end 3693 3694 if tm 3695 unless (assoc = current.associations).has_key?(assoc_name) 3696 assoc[assoc_name] = [] 3697 end 3698 assoc[assoc_name].push(rec) 3699 rec.associations[rcm] = current if rcm 3700 else 3701 current.associations[assoc_name] ||= rec 3702 end 3703 # Recurse into dependencies of the current object 3704 _load(deps, rec, h) unless deps.empty? 3705 end 3706 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 3709 def hfor(ta, h) 3710 out = {} 3711 @column_maps[ta].each{|ca, c| out[c] = h[ca]} 3712 out 3713 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 3717 def hkey(h) 3718 h.sort_by{|x| x[0]} 3719 end
Return the subhash for the master table by parsing the values out of the main hash h
# File lib/sequel/model/associations.rb 3722 def master_hfor(h) 3723 out = {} 3724 @master_column_map.each{|ca, c| out[c] = h[ca]} 3725 out 3726 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 3729 def master_pk(h) 3730 x = @master_primary_keys 3731 if x.is_a?(Array) 3732 unless x == [] 3733 x = x.map{|ca| h[ca]} 3734 x if x.all? 3735 end 3736 else 3737 h[x] 3738 end 3739 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 3742 def pk(ta, h) 3743 x = primary_keys[ta] 3744 if x.is_a?(Array) 3745 unless x == [] 3746 x = x.map{|ca| h[ca]} 3747 x if x.all? 3748 end 3749 else 3750 h[x] 3751 end 3752 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 3759 def post_process(records, dependency_map) 3760 records.each do |record| 3761 dependency_map.each do |ta, deps| 3762 assoc_name = alias_map[ta] 3763 list = record.public_send(assoc_name) 3764 rec_list = if type_map[ta] 3765 list.uniq! 3766 if lo = limit_map[ta] 3767 limit, offset = lo 3768 offset ||= 0 3769 if type_map[ta] == :offset 3770 [record.associations[assoc_name] = list[offset]] 3771 else 3772 list.replace(list[(offset)..(limit ? (offset)+limit-1 : -1)] || []) 3773 end 3774 else 3775 list 3776 end 3777 elsif list 3778 [list] 3779 else 3780 [] 3781 end 3782 record.send(:run_association_callbacks, reflection_map[ta], :after_load, list) if after_load_map[ta] 3783 post_process(rec_list, deps) if !rec_list.empty? && !deps.empty? 3784 end 3785 end 3786 end