aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/join_dependency.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/associations/join_dependency.rb')
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb205
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