# Eager loading makes it so that you can load all associated records for a # set of objects in a single query, instead of a separate query for each object. # # Two separate implementations are provided. .eager should be used most of the # time, as it loads associated records using one query per association. However, # it does not allow you the ability to filter based on columns in associated tables. .eager_graph loads # all records in one query. Using .eager_graph you can filter based on columns in associated # tables. However, .eager_graph can be much slower than .eager, especially if multiple # *_to_many associations are joined. # # You can cascade the eager loading (loading associations' associations) # with no limit to the depth of the cascades. You do this by passing a hash to .eager or .eager_graph # with the keys being associations of the current model and values being # associations of the model associated with the current model via the key. # # You cannot eagerly load an association with a block argument, as the block argument is # evaluated in terms of a specific instance of the model, and no specific instance exists. # # The arguments can be symbols or hashes with symbol keys (for cascaded # eager loading). Examples: # # Album.eager(:artist).all # Album.eager_graph(:artist).all # Album.eager(:artist, :genre).all # Album.eager_graph(:artist, :genre).all # Album.eager(:artist).eager(:genre).all # Album.eager_graph(:artist).eager(:genre).all # Artist.eager(:albums=>:tracks).all # Artist.eager_graph(:albums=>:tracks).all # Artist.eager(:albums=>{:tracks=>:genre}).all # Artist.eager_graph(:albums=>{:tracks=>:genre}).all module Sequel::Model::Associations::EagerLoading # Add the .eager! and .eager_graph! mutation methods to the dataset. def self.extended(obj) obj.def_mutation_method(:eager, :eager_graph) end # The preferred eager loading method. Loads all associated records using one # query for each association. # # The basic idea for how it works is that the dataset is first loaded normally. # Then it goes through all associations that have been specified via .eager. # It loads each of those associations separately, then associates them back # to the original dataset via primary/foreign keys. Due to the necessity of # all objects being present, you need to use .all to use eager loading, as it # can't work with .each. # # This implementation avoids the complexity of extracting an object graph out # of a single dataset, by building the object graph out of multiple datasets, # one for each association. By using a separate dataset for each association, # it avoids problems such as aliasing conflicts and creating cartesian product # result sets if multiple *_to_many eager associations are requested. # # One limitation of using this method is that you cannot filter the dataset # based on values of columns in an associated table, since the associations are loaded # in separate queries. To do that you need to load all associations in the # same query, and extract an object graph from the results of that query. If you # need to filter based on columns in associated tables, look at .eager_graph # or join the tables you need to filter on manually. # # Each association's order, if definied, is respected. Eager also works # on a limited dataset. def eager(*associations) model = check_model opt = @opts[:eager] opt = opt ? opt.dup : {} associations.flatten.each do |association| case association when Symbol check_association(model, association) opt[association] = nil when Hash association.keys.each{|assoc| check_association(model, assoc)} opt.merge!(association) else raise(ArgumentError, 'Associations must be in the form of a symbol or hash') end end clone(:eager=>opt) end # The secondary eager loading method. Loads all associations in a single query. This # method should only be used if you need to filter based on columns in associated tables. # # This method builds an object graph using the .graph method. Then it uses the graph # to build the associations, and finally replaces the graph with a simple array # of model objects. # # Be very careful when using this with multiple *_to_many associations, as you can # create large cartesian products. If you must graph multiple *_to_many associations, # make sure your filters are specific if you have a large database. # # This does not respect each association's order, as all associations are loaded in # a single query. If you want to order the results, you must manually call .order. # # eager_graph probably won't work the way you suspect with limit, unless you are # only graphing many_to_one associations. def eager_graph(*associations) model = check_model table_name = model.table_name ds = if @opts[:eager_graph] self else # Each of the following have a symbol key for the table alias, with the following values: # :requirements - array of requirements for this association # :alias_association_type_map - the type of association for this association # :alias_association_name_map - the name of the association for this association clone(:eager_graph=>{:requirements=>{}, :master=>model.table_name, :alias_association_type_map=>{}, :alias_association_name_map=>{}, :reciprocals=>{}}) end ds.eager_graph_associations(ds, model, table_name, [], *associations) end protected # Call graph on the association with the correct arguments, # update the eager_graph data structure, and recurse into # eager_graph_associations if there are any passed in associations # (which would be dependencies of the current association) # # Arguments: # * ds - Current dataset # * model - Current Model # * ta - table_alias used for the parent association # * requirements - an array, used as a stack for requirements # * r - association reflection for the current association # * *associations - any associations dependent on this one def eager_graph_association(ds, model, ta, requirements, r, *associations) klass = model.send(:associated_class, r) assoc_name = r[:name] assoc_table_alias = ds.eager_unique_table_alias(ds, assoc_name) ds = case assoc_type = r[:type] when :many_to_one ds.graph(klass, {klass.primary_key=>:"#{ta}__#{r[:key]}"}, :table_alias=>assoc_table_alias) when :one_to_many ds = ds.graph(klass, {r[:key]=>:"#{ta}__#{model.primary_key}"}, :table_alias=>assoc_table_alias) # We only load reciprocals for one_to_many associations, as other reciprocals don't make sense ds.opts[:eager_graph][:reciprocals][assoc_table_alias] = model.send(:reciprocal_association, r) ds when :many_to_many ds = ds.graph(r[:join_table], {r[:left_key]=>:"#{ta}__#{model.primary_key}"}, :select=>false, :table_alias=>ds.eager_unique_table_alias(ds, r[:join_table])) ds.graph(klass, {klass.primary_key=>r[:right_key]}, :table_alias=>assoc_table_alias) end eager_graph = ds.opts[:eager_graph] eager_graph[:requirements][assoc_table_alias] = requirements.dup eager_graph[:alias_association_name_map][assoc_table_alias] = assoc_name eager_graph[:alias_association_type_map][assoc_table_alias] = assoc_type ds = ds.eager_graph_associations(ds, klass, assoc_table_alias, requirements + [assoc_table_alias], *associations) unless associations.empty? ds end # Check the associations are valid for the given model. # Call eager_graph_association on each association. # # Arguments: # * ds - Current dataset # * model - Current Model # * ta - table_alias used for the parent association # * requirements - an array, used as a stack for requirements # * *associations - the associations to add to the graph def eager_graph_associations(ds, model, ta, requirements, *associations) return ds if associations.empty? associations.flatten.each do |association| ds = case association when Symbol ds.eager_graph_association(ds, model, ta, requirements, check_association(model, association)) when Hash association.each do |assoc, assoc_assocs| ds = ds.eager_graph_association(ds, model, ta, requirements, check_association(model, assoc), assoc_assocs) end ds else raise(ArgumentError, 'Associations must be in the form of a symbol or hash') end end ds end # Build associations out of the array of returned object graphs. def eager_graph_build_associations(record_graphs) # Dup the tables that will be used, so that self is not modified. eager_graph = @opts[:eager_graph] master = eager_graph[:master] requirements = eager_graph[:requirements] alias_map = eager_graph[:alias_association_name_map] type_map = eager_graph[:alias_association_type_map] reciprocal_map = eager_graph[:reciprocals] # Make dependency map hash out of requirements array for each association. # This builds a tree of dependencies that will be used for recursion # to ensure that all parts of the object graph are loaded into the # appropriate subordinate association. dependency_map = {} # Sort the associations be requirements length, so that # requirements are added to the dependency hash before their # dependencies. requirements.sort_by{|a| a[1].length}.each do |ta, deps| if deps.empty? dependency_map[ta] = {} else deps = deps.dup hash = dependency_map[deps.shift] deps.each do |dep| hash = hash[dep] end hash[ta] = {} end end # This mapping is used to make sure that duplicate entries in the # result set are mapped to a single record. For example, using a # single one_to_many association with 10 associated records, # the main object will appear in the object graph 10 times. # We map by primary key, if available, or by the object's entire values, # if not. The mapping must be per table, so create sub maps for each table # alias. records_map = {master=>{}} alias_map.keys.each{|ta| records_map[ta] = {}} # This will hold the final record set that we will be replacing the object graph with. records = [] record_graphs.each do |record_graph| primary_record = record_graph[master] key = primary_record.pk || primary_record.values.sort_by{|x| x[0].to_s} if cached_pr = records_map[master][key] primary_record = cached_pr else records_map[master][key] = primary_record # Only add it to the list of records to return if it is a new record records.push(primary_record) end # Build all associations for the current object and it's dependencies eager_graph_build_associations_graph(dependency_map, alias_map, type_map, reciprocal_map, records_map, primary_record, record_graph) end # Remove duplicate records from all associations if this graph could possibly be a cartesian product eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map) if type_map.reject{|k,v| v == :many_to_one}.length > 1 # Replace the array of object graphs with an array of model objects record_graphs.replace(records) end # Creates a unique table alias that hasn't already been used in the query. # Will either be the table_alias itself or table_alias_N for some integer # N (starting at 0 and increasing until an unused one is found). def eager_unique_table_alias(ds, table_alias) if (graph = ds.opts[:graph]) && (table_aliases = graph[:table_aliases]) && (table_aliases.include?(table_alias)) i = 0 loop do ta = :"#{table_alias}_#{i}" return ta unless table_aliases[ta] i += 1 end else table_alias end end private # Make sure a standard (non-polymorphic model) is used for this dataset, and return the model def check_model raise(ArgumentError, 'No model for this dataset') unless @opts[:models] && model = @opts[:models][nil] model end # Make sure the association is valid for this model, and return the association's reflection def check_association(model, association) raise(ArgumentError, 'Invalid association') unless reflection = model.association_reflection(association) raise(ArgumentError, 'Cannot eagerly load associations with block arguments') if reflection[:block] reflection end # Build associations for the current object. This is called recursively # to build object's dependencies. def eager_graph_build_associations_graph(dependency_map, alias_map, type_map, reciprocal_map, records_map, current, record_graph) return if dependency_map.empty? # Don't clobber the instance variable array for *_to_many associations if it has already been setup dependency_map.keys.each do |ta| current.instance_variable_set("@#{alias_map[ta]}", type_map[ta] == :many_to_one ? :null : []) unless current.instance_variable_get("@#{alias_map[ta]}") end dependency_map.each do |ta, deps| next unless rec = record_graph[ta] key = rec.pk || rec.values.sort_by{|x| x[0].to_s} if cached_rec = records_map[ta][key] rec = cached_rec else records_map[ta][rec.pk] = rec end ivar = "@#{alias_map[ta]}" case assoc_type = type_map[ta] when :many_to_one current.instance_variable_set(ivar, rec) else list = current.instance_variable_get(ivar) list.push(rec) if (assoc_type == :one_to_many) && (reciprocal = reciprocal_map[ta]) rec.instance_variable_set(reciprocal, current) end end # Recurse into dependencies of the current object eager_graph_build_associations_graph(deps, alias_map, type_map, reciprocal_map, records_map, rec, record_graph) end end # If the result set is the result of a cartesian product, then it is possible that # there a multiple records for each association when there should only be one. def eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map) records.each do |record| dependency_map.each do |ta, deps| list = if type_map[ta] == :many_to_one item = record.send(alias_map[ta]) [item] if item else list = record.send(alias_map[ta]) list.uniq! # Recurse into dependencies list.each{|rec| eager_graph_make_associations_unique(rec, deps, alias_map, type_map)} end end end end # Eagerly load all specified associations def eager_load(a) return if a.empty? # Current model class model = @opts[:models][nil] # All associations to eager load eager_assoc = @opts[:eager] # Key is foreign/primary key name symbol # Value is hash with keys being foreign/primary key values (generally integers) # and values being an array of current model objects with that # specific foreign/primary key key_hash = {} # array of attribute_values keys to monitor keys = [] # Reflections for all associations to eager load reflections = eager_assoc.keys.collect{|assoc| model.association_reflection(assoc)} # Populate keys to monitor reflections.each do |reflection| key = reflection[:type] == :many_to_one ? reflection[:key] : model.primary_key next if key_hash[key] key_hash[key] = {} keys << key end # Associate each object with every key being monitored a.each do |r| keys.each do |key| ((key_hash[key][r[key]] ||= []) << r) if r[key] end end # Iterate through eager associations and assign instance variables # for the association for all model objects reflections.each do |reflection| assoc_class = model.send(:associated_class, reflection) assoc_name = reflection[:name] # Proc for setting cascaded eager loading cascade = Proc.new do |d| if c = eager_assoc[assoc_name] d = d.eager(c) end if c = reflection[:eager] d = d.eager(c) end d end case rtype = reflection[:type] when :many_to_one key = reflection[:key] h = key_hash[key] keys = h.keys # No records have the foreign key set for this association, so skip it next unless keys.length > 0 ds = assoc_class.filter(assoc_class.primary_key=>keys) ds = cascade.call(ds) ds.all do |assoc_object| h[assoc_object.pk].each do |object| object.instance_variable_set(:"@#{assoc_name}", assoc_object) end end when :one_to_many, :many_to_many if rtype == :one_to_many fkey = key = reflection[:key] h = key_hash[model.primary_key] reciprocal = model.send(:reciprocal_association, reflection) ds = assoc_class.filter(key=>h.keys) else assoc_table = assoc_class.table_name left = reflection[:left_key] right = reflection[:right_key] right_pk = (reflection[:right_primary_key] || :"#{assoc_table}__#{assoc_class.primary_key}") join_table = reflection[:join_table] fkey = (reflection[:left_key_alias] ||= :"x_foreign_key_x") table_selection = (reflection[:select] ||= assoc_table.*) key_selection = (reflection[:left_key_select] ||= :"#{join_table}__#{left}___#{fkey}") h = key_hash[model.primary_key] ds = assoc_class.select(table_selection, key_selection).inner_join(join_table, right=>right_pk, left=>h.keys) end if order = reflection[:order] ds = ds.order(order) end ds = cascade.call(ds) ivar = :"@#{assoc_name}" h.values.each do |object_array| object_array.each do |object| object.instance_variable_set(ivar, []) end end ds.all do |assoc_object| fk = if rtype == :many_to_many assoc_object.values.delete(fkey) else assoc_object[fkey] end h[fk].each do |object| object.instance_variable_get(ivar) << assoc_object assoc_object.instance_variable_set(reciprocal, object) if reciprocal end end end end end # Build associations from the graph if .eager_graph was used, # and/or load other associations if .eager was used. def post_load(all_records) eager_graph_build_associations(all_records) if @opts[:eager_graph] eager_load(all_records) if @opts[:eager] end end