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