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