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