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