aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb205
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb29
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_base.rb8
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_part.rb44
-rw-r--r--activerecord/lib/active_record/relation.rb3
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb27
-rw-r--r--activerecord/test/cases/associations/join_dependency_test.rb8
7 files changed, 151 insertions, 173 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
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
index 3af613d2d1..a6a7b447cc 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -4,43 +4,30 @@ module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinAssociation < JoinPart # :nodoc:
- include JoinHelper
-
# The reflection of the association represented
attr_reader :reflection
# What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
attr_accessor :join_type
- # These implement abstract methods from the superclass
- attr_reader :aliased_prefix
-
- attr_reader :tables
- attr_reader :alias_tracker
+ attr_accessor :tables
- delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection
-
- def initialize(reflection, index, parent, join_type, alias_tracker)
- super(reflection.klass, parent)
+ def initialize(reflection, join_type)
+ super(reflection.klass)
@reflection = reflection
- @alias_tracker = alias_tracker
@join_type = join_type
- @aliased_prefix = "t#{ index }"
- @tables = construct_tables.reverse
+ @tables = nil
end
- def parent_table_name; parent.table_name; end
- alias :alias_suffix :parent_table_name
-
def match?(other)
return true if self == other
super && reflection == other.reflection
end
- def join_constraints
+ def join_constraints(parent, tables)
joins = []
- tables = @tables.dup
+ tables = tables.dup
foreign_table = parent.table
foreign_klass = parent.base_klass
@@ -49,7 +36,7 @@ module ActiveRecord
# The chain starts with the target table, but we want to end with it here (makes
# more sense in this context), so we reverse
- chain.reverse_each do |reflection|
+ reflection.chain.reverse_each do |reflection|
table = tables.shift
klass = reflection.klass
@@ -88,7 +75,7 @@ module ActiveRecord
constraint = constraint.and rel.arel.constraints
end
- joins << join(table, constraint)
+ joins << table.create_join(table, table.create_on(constraint), join_type)
# The current table in this iteration becomes the foreign table in the next
foreign_table, foreign_klass = table, klass
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
index adc9f63aec..3a26c25737 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
@@ -4,19 +4,11 @@ module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinBase < JoinPart # :nodoc:
- def initialize(klass)
- super(klass, nil)
- end
-
def match?(other)
return true if self == other
super && base_klass == other.base_klass
end
- def aliased_prefix
- "t0"
- end
-
def table
base_klass.arel_table
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
index e6da4d3c9e..e22232168d 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
@@ -10,10 +10,6 @@ module ActiveRecord
class JoinPart # :nodoc:
include Enumerable
- # A JoinBase instance representing the active record we are joining onto.
- # (So in Author.has_many :posts, the Author would be that base record.)
- attr_reader :parent
-
# The Active Record class which this join part is associated 'about'; for a JoinBase
# this is the actual base model, for a JoinAssociation this is the target model of the
# association.
@@ -21,10 +17,8 @@ module ActiveRecord
delegate :table_name, :column_names, :primary_key, :to => :base_klass
- def initialize(base_klass, parent)
+ def initialize(base_klass)
@base_klass = base_klass
- @parent = parent
- @cached_record = {}
@column_names_with_alias = nil
@children = []
end
@@ -42,43 +36,17 @@ module ActiveRecord
children.each { |child| child.each(&block) }
end
- def aliased_table
- Arel::Nodes::TableAlias.new table, aliased_table_name
- end
-
# An Arel::Table for the active_record
def table
raise NotImplementedError
end
- # The prefix to be used when aliasing columns in the active_record's table
- def aliased_prefix
- raise NotImplementedError
- end
-
# The alias for the active_record's table
def aliased_table_name
raise NotImplementedError
end
- # The alias for the primary key of the active_record's table
- def aliased_primary_key
- "#{aliased_prefix}_r0"
- end
-
- # An array of [column_name, alias] pairs for the table
- def column_names_with_alias
- unless @column_names_with_alias
- @column_names_with_alias = []
-
- column_names.each_with_index do |column_name, i|
- @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
- end
- end
- @column_names_with_alias
- end
-
- def extract_record(row)
+ def extract_record(row, column_names_with_alias)
# This code is performance critical as it is called per row.
# see: https://github.com/rails/rails/pull/12185
hash = {}
@@ -95,12 +63,8 @@ module ActiveRecord
hash
end
- def record_id(row)
- row[aliased_primary_key]
- end
-
- def instantiate(row)
- @cached_record[record_id(row)] ||= base_klass.instantiate(extract_record(row))
+ def instantiate(row, aliases)
+ base_klass.instantiate(extract_record(row, aliases))
end
end
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index cfaf566ec4..c49f9a9f3d 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -507,8 +507,7 @@ module ActiveRecord
visitor = connection.visitor
if eager_loading?
- join_dependency = construct_join_dependency
- relation = construct_relation_for_association_find(join_dependency)
+ find_with_associations { |rel| relation = rel }
end
ast = relation.arel.ast
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index fe75a32545..a2c18c5226 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -201,7 +201,7 @@ module ActiveRecord
conditions = conditions.id if Base === conditions
return false if !conditions
- relation = construct_relation_for_association_find(construct_join_dependency)
+ relation = apply_join_dependency(self, construct_join_dependency)
return false if ActiveRecord::NullRelation === relation
relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1)
@@ -242,17 +242,25 @@ module ActiveRecord
def find_with_associations
join_dependency = construct_join_dependency
- relation = construct_relation_for_association_find(join_dependency)
- if ActiveRecord::NullRelation === relation
- []
+
+ aliases = join_dependency.aliases
+ relation = select aliases.columns
+ relation = apply_join_dependency(relation, join_dependency)
+
+ if block_given?
+ yield relation
else
- rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup)
- join_dependency.instantiate(rows)
+ if ActiveRecord::NullRelation === relation
+ []
+ else
+ rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup)
+ join_dependency.instantiate(rows, aliases)
+ end
end
end
def construct_join_dependency(joins = [])
- including = (eager_load_values + includes_values).uniq
+ including = eager_load_values + includes_values
ActiveRecord::Associations::JoinDependency.new(@klass, including, joins)
end
@@ -260,11 +268,6 @@ module ActiveRecord
apply_join_dependency(self, construct_join_dependency(arel.froms.first))
end
- def construct_relation_for_association_find(join_dependency)
- relation = select(join_dependency.columns)
- apply_join_dependency(relation, join_dependency)
- end
-
def apply_join_dependency(relation, join_dependency)
relation = relation.except(:includes, :eager_load, :preload)
relation = relation.joins join_dependency
diff --git a/activerecord/test/cases/associations/join_dependency_test.rb b/activerecord/test/cases/associations/join_dependency_test.rb
deleted file mode 100644
index 08c166dc33..0000000000
--- a/activerecord/test/cases/associations/join_dependency_test.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-require "cases/helper"
-require 'models/edge'
-
-class JoinDependencyTest < ActiveRecord::TestCase
- def test_column_names_with_alias_handles_nil_primary_key
- assert_equal Edge.column_names, ActiveRecord::Associations::JoinDependency::JoinBase.new(Edge).column_names_with_alias.map(&:first)
- end
-end \ No newline at end of file