diff options
Diffstat (limited to 'activerecord/lib/active_record/associations/join_dependency.rb')
-rw-r--r-- | activerecord/lib/active_record/associations/join_dependency.rb | 205 |
1 files changed, 123 insertions, 82 deletions
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 6e08f67286..7dffc3e3e5 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -53,7 +53,6 @@ module ActiveRecord # def initialize(base, associations, joins) @base_klass = base - @table_joins = joins @join_root = JoinBase.new(base) @alias_tracker = AliasTracker.new(base.connection, joins) @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1 @@ -80,37 +79,105 @@ module ActiveRecord end def join_constraints - join_root.children.flat_map { |c| c.flat_map(&:join_constraints) } + make_joins join_root end - def columns - join_root.collect { |join_part| - table = join_part.aliased_table - join_part.column_names_with_alias.collect{ |column_name, aliased_name| - table[column_name].as Arel.sql(aliased_name) + class Aliases + def initialize(tables) + @tables = tables + @alias_cache = tables.each_with_object({}) { |table,h| + h[table.name] = table.columns.each_with_object({}) { |column,i| + i[column.name] = column.alias + } } - }.flatten + @name_and_alias_cache = tables.each_with_object({}) { |table,h| + h[table.name] = table.columns.map { |column| + [column.name, column.alias] + } + } + end + + def columns + @tables.flat_map { |t| t.column_aliases } + end + + # An array of [column_name, alias] pairs for the table + def column_aliases(table) + @name_and_alias_cache[table] + end + + def column_alias(table, column) + @alias_cache[table][column] + end + + class Table < Struct.new(:name, :alias, :columns) + def table + Arel::Nodes::TableAlias.new name, self.alias + end + + def column_aliases + t = table + columns.map { |column| t[column.name].as Arel.sql column.alias } + end + end + Column = Struct.new(:name, :alias) end - def instantiate(result_set) - primary_key = join_root.aliased_primary_key - parents = {} + def aliases + Aliases.new join_root.each_with_index.map { |join_part,i| + columns = join_part.column_names.each_with_index.map { |column_name,j| + Aliases::Column.new column_name, "t#{i}_r#{j}" + } + Aliases::Table.new(join_part.table, join_part.aliased_table_name, columns) + } + end + def instantiate(result_set, aliases) + primary_key = aliases.column_alias(join_root.table, join_root.primary_key) type_caster = result_set.column_type primary_key - records = result_set.map { |row_hash| + seen = Hash.new { |h,parent_klass| + h[parent_klass] = Hash.new { |i,parent_id| + i[parent_id] = Hash.new { |j,child_klass| j[child_klass] = {} } + } + } + + model_cache = Hash.new { |h,klass| h[klass] = {} } + parents = model_cache[join_root] + column_aliases = aliases.column_aliases join_root.table + + result_set.each { |row_hash| primary_id = type_caster.type_cast row_hash[primary_key] - parent = parents[primary_id] ||= join_root.instantiate(row_hash) - construct(parent, join_root, row_hash, result_set) - parent - }.uniq + parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases) + construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) + } - remove_duplicate_results!(base_klass, records, join_root.children) - records + parents.values end private + def make_joins(node) + node.children.flat_map { |child| + child.join_constraints(node, child.tables).concat make_joins(child) + } + end + + def construct_tables!(parent, node) + node.tables = node.reflection.chain.map { |reflection| + alias_tracker.aliased_table_for( + reflection.table_name, + table_alias_for(reflection, parent, reflection != node.reflection) + ) + }.reverse + end + + def table_alias_for(reflection, parent, join) + name = "#{reflection.plural_name}_#{parent.table_name}" + name << "_join" if join + name + end + def merge_node(left, right) intersection, missing = right.children.map { |node1| [left.children.find { |node2| node1.match? node2 }, node1] @@ -127,28 +194,6 @@ module ActiveRecord dup end - def remove_duplicate_results!(base, records, associations) - associations.each do |node| - reflection = base.reflect_on_association(node.name) - remove_uniq_by_reflection(reflection, records) - - parent_records = [] - records.each do |record| - if descendant = record.send(reflection.name) - if reflection.collection? - parent_records.concat descendant.target.uniq - else - parent_records << descendant - end - end - end - - unless parent_records.empty? - remove_duplicate_results!(reflection.klass, parent_records, node.children) - end - end - end - def find_reflection(klass, name) klass.reflect_on_association(name) or raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?" @@ -163,17 +208,6 @@ module ActiveRecord end end - def build_scalar(reflection, parent, join_type) - join_association = build_join_association(reflection, parent, join_type) - parent.children << join_association - end - - def remove_uniq_by_reflection(reflection, records) - if reflection && reflection.collection? - records.each { |record| record.send(reflection.name).target.uniq! } - end - end - def build_join_association(reflection, parent, join_type) reflection.check_validity! @@ -181,48 +215,55 @@ module ActiveRecord raise EagerLoadPolymorphicError.new(reflection) end - JoinAssociation.new(reflection, join_root.to_a.length, parent, join_type, alias_tracker) + node = JoinAssociation.new(reflection, join_type) + construct_tables!(parent, node) + node end - def construct(ar_parent, parent, row, rs) + def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) + primary_id = ar_parent.id + parent.children.each do |node| - association = construct_association(ar_parent, parent, node, row, rs) - construct(association, node, row, rs) if association - end - end + if node.reflection.collection? + other = ar_parent.association(node.reflection.name) + other.loaded! + else + if ar_parent.association_cache.key?(node.reflection.name) + model = ar_parent.association(node.reflection.name).target + construct(model, node, row, rs, seen, model_cache, aliases) + next + end + end - def construct_association(record, parent, join_part, row, rs) - caster = rs.column_type(parent.aliased_primary_key) - row_id = caster.type_cast row[parent.aliased_primary_key] + key = aliases.column_alias(node.table, node.primary_key) + id = row[key] + next if id.nil? - return if record.id != row_id + model = seen[parent.base_klass][primary_id][node.base_klass][id] - macro = join_part.reflection.macro - if macro == :has_one - return record.association(join_part.reflection.name).target if record.association_cache.key?(join_part.reflection.name) - association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? - set_target_and_inverse(join_part, association, record) - else - association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? - case macro - when :has_many - other = record.association(join_part.reflection.name) - other.loaded! - other.target.push(association) if association - other.set_inverse_instance(association) - when :belongs_to - set_target_and_inverse(join_part, association, record) + if model + construct(model, node, row, rs, seen, model_cache, aliases) else - raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}" + model = construct_model(ar_parent, node, row, model_cache, id, aliases) + seen[parent.base_klass][primary_id][node.base_klass][id] = model + construct(model, node, row, rs, seen, model_cache, aliases) end end - association end - def set_target_and_inverse(join_part, association, record) - other = record.association(join_part.reflection.name) - other.target = association - other.set_inverse_instance(association) + def construct_model(record, node, row, model_cache, id, aliases) + model = model_cache[node][id] ||= node.instantiate(row, + aliases.column_aliases(node.table)) + other = record.association(node.reflection.name) + + if node.reflection.collection? + other.target.push(model) + else + other.target = model + end + + other.set_inverse_instance(model) + model end end end |