mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Fix eager loading of associations causing table name collisions
[#4463 state:committed] Signed-off-by: Jeremy Kemper <jeremy@bitsweat.net>
This commit is contained in:
parent
4e75cc59e7
commit
e33d304975
4 changed files with 115 additions and 53 deletions
|
@ -1737,6 +1737,14 @@ module ActiveRecord
|
||||||
build(associations)
|
build(associations)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def graft(*associations)
|
||||||
|
associations.each do |association|
|
||||||
|
join_associations.detect {|a| association == a} ||
|
||||||
|
build(association.reflection.name, association.find_parent_in(self), association.join_class)
|
||||||
|
end
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
def join_associations
|
def join_associations
|
||||||
@joins[1..-1].to_a
|
@joins[1..-1].to_a
|
||||||
end
|
end
|
||||||
|
@ -1745,6 +1753,16 @@ module ActiveRecord
|
||||||
@joins[0]
|
@joins[0]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def count_aliases_from_table_joins(name)
|
||||||
|
quoted_name = join_base.active_record.connection.quote_table_name(name.downcase)
|
||||||
|
join_sql = join_base.table_joins.to_s.downcase
|
||||||
|
join_sql.blank? ? 0 :
|
||||||
|
# Table names
|
||||||
|
join_sql.scan(/join(?:\s+\w+)?\s+#{quoted_name}\son/).size +
|
||||||
|
# Table aliases
|
||||||
|
join_sql.scan(/join(?:\s+\w+)?\s+\S+\s+#{quoted_name}\son/).size
|
||||||
|
end
|
||||||
|
|
||||||
def instantiate(rows)
|
def instantiate(rows)
|
||||||
rows.each_with_index do |row, i|
|
rows.each_with_index do |row, i|
|
||||||
primary_id = join_base.record_id(row)
|
primary_id = join_base.record_id(row)
|
||||||
|
@ -1789,22 +1807,22 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
def build(associations, parent = nil)
|
def build(associations, parent = nil, join_class = Arel::InnerJoin)
|
||||||
parent ||= @joins.last
|
parent ||= @joins.last
|
||||||
case associations
|
case associations
|
||||||
when Symbol, String
|
when Symbol, String
|
||||||
reflection = parent.reflections[associations.to_s.intern] or
|
reflection = parent.reflections[associations.to_s.intern] or
|
||||||
raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
|
raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
|
||||||
@reflections << reflection
|
@reflections << reflection
|
||||||
@joins << build_join_association(reflection, parent)
|
@joins << build_join_association(reflection, parent).with_join_class(join_class)
|
||||||
when Array
|
when Array
|
||||||
associations.each do |association|
|
associations.each do |association|
|
||||||
build(association, parent)
|
build(association, parent, join_class)
|
||||||
end
|
end
|
||||||
when Hash
|
when Hash
|
||||||
associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
|
associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
|
||||||
build(name, parent)
|
build(name, parent, join_class)
|
||||||
build(associations[name])
|
build(associations[name], nil, join_class)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
raise ConfigurationError, associations.inspect
|
raise ConfigurationError, associations.inspect
|
||||||
|
@ -1881,6 +1899,12 @@ module ActiveRecord
|
||||||
@table_joins = joins
|
@table_joins = joins
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ==(other)
|
||||||
|
other.is_a?(JoinBase) &&
|
||||||
|
other.active_record == active_record &&
|
||||||
|
other.table_joins == table_joins
|
||||||
|
end
|
||||||
|
|
||||||
def aliased_prefix
|
def aliased_prefix
|
||||||
"t0"
|
"t0"
|
||||||
end
|
end
|
||||||
|
@ -1946,6 +1970,27 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ==(other)
|
||||||
|
other.is_a?(JoinAssociation) &&
|
||||||
|
other.reflection == reflection &&
|
||||||
|
other.parent == parent
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_parent_in(other_join_dependency)
|
||||||
|
other_join_dependency.joins.detect do |join|
|
||||||
|
self.parent == join
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def join_class
|
||||||
|
@join_class ||= Arel::InnerJoin
|
||||||
|
end
|
||||||
|
|
||||||
|
def with_join_class(join_class)
|
||||||
|
@join_class = join_class
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
def association_join
|
def association_join
|
||||||
return @join if @join
|
return @join if @join
|
||||||
|
|
||||||
|
@ -2045,27 +2090,25 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def join_relation(joining_relation, join = nil)
|
def join_relation(joining_relation, join = nil)
|
||||||
if (relations = relation).is_a?(Array)
|
joining_relation.joins(self.with_join_class(Arel::OuterJoin))
|
||||||
joining_relation.joins(Relation::JoinOperation.new(relations.first, Arel::OuterJoin, association_join.first)).
|
|
||||||
joins(Relation::JoinOperation.new(relations.last, Arel::OuterJoin, association_join.last))
|
|
||||||
else
|
|
||||||
joining_relation.joins(Relation::JoinOperation.new(relations, Arel::OuterJoin, association_join))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def aliased_table_name_for(name, suffix = nil)
|
def aliased_table_name_for(name, suffix = nil)
|
||||||
if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{active_record.connection.quote_table_name name.downcase}\son}
|
if @join_dependency.table_aliases[name].zero?
|
||||||
@join_dependency.table_aliases[name] += 1
|
@join_dependency.table_aliases[name] = @join_dependency.count_aliases_from_table_joins(name)
|
||||||
end
|
end
|
||||||
|
|
||||||
unless @join_dependency.table_aliases[name].zero?
|
if !@join_dependency.table_aliases[name].zero? # We need an alias
|
||||||
# if the table name has been used, then use an alias
|
|
||||||
name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
|
name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
|
||||||
table_index = @join_dependency.table_aliases[name]
|
|
||||||
@join_dependency.table_aliases[name] += 1
|
@join_dependency.table_aliases[name] += 1
|
||||||
name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0
|
if @join_dependency.table_aliases[name] == 1 # First time we've seen this name
|
||||||
|
# Also need to count the aliases from the table_aliases to avoid incorrect count
|
||||||
|
@join_dependency.table_aliases[name] += @join_dependency.count_aliases_from_table_joins(name)
|
||||||
|
end
|
||||||
|
table_index = @join_dependency.table_aliases[name]
|
||||||
|
name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index}" if table_index > 1
|
||||||
else
|
else
|
||||||
@join_dependency.table_aliases[name] += 1
|
@join_dependency.table_aliases[name] += 1
|
||||||
end
|
end
|
||||||
|
|
|
@ -188,7 +188,6 @@ module ActiveRecord
|
||||||
def construct_relation_for_association_calculations
|
def construct_relation_for_association_calculations
|
||||||
including = (@eager_load_values + @includes_values).uniq
|
including = (@eager_load_values + @includes_values).uniq
|
||||||
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, arel.joins(arel))
|
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, arel.joins(arel))
|
||||||
|
|
||||||
relation = except(:includes, :eager_load, :preload)
|
relation = except(:includes, :eager_load, :preload)
|
||||||
apply_join_dependency(relation, join_dependency)
|
apply_join_dependency(relation, join_dependency)
|
||||||
end
|
end
|
||||||
|
|
|
@ -80,48 +80,14 @@ module ActiveRecord
|
||||||
@arel ||= build_arel
|
@arel ||= build_arel
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_arel
|
def custom_join_sql(*joins)
|
||||||
arel = table
|
arel = table
|
||||||
|
|
||||||
joined_associations = []
|
|
||||||
association_joins = []
|
|
||||||
|
|
||||||
joins = @joins_values.map {|j| j.respond_to?(:strip) ? j.strip : j}.uniq
|
|
||||||
|
|
||||||
# Build association joins first
|
|
||||||
joins.each do |join|
|
|
||||||
association_joins << join if [Hash, Array, Symbol].include?(join.class) && !array_of_strings?(join)
|
|
||||||
end
|
|
||||||
|
|
||||||
if association_joins.any?
|
|
||||||
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins.uniq, nil)
|
|
||||||
to_join = []
|
|
||||||
|
|
||||||
join_dependency.join_associations.each do |association|
|
|
||||||
if (association_relation = association.relation).is_a?(Array)
|
|
||||||
to_join << [association_relation.first, association.association_join.first]
|
|
||||||
to_join << [association_relation.last, association.association_join.last]
|
|
||||||
else
|
|
||||||
to_join << [association_relation, association.association_join]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
to_join.each do |tj|
|
|
||||||
unless joined_associations.detect {|ja| ja[0] == tj[0] && ja[1] == tj[1] }
|
|
||||||
joined_associations << tj
|
|
||||||
arel = arel.join(tj[0]).on(*tj[1])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
joins.each do |join|
|
joins.each do |join|
|
||||||
next if join.blank?
|
next if join.blank?
|
||||||
|
|
||||||
@implicit_readonly = true
|
@implicit_readonly = true
|
||||||
|
|
||||||
case join
|
case join
|
||||||
when Relation::JoinOperation
|
|
||||||
arel = arel.join(join.relation, join.join_class).on(*join.on)
|
|
||||||
when Hash, Array, Symbol
|
when Hash, Array, Symbol
|
||||||
if array_of_strings?(join)
|
if array_of_strings?(join)
|
||||||
join_string = join.join(' ')
|
join_string = join.join(' ')
|
||||||
|
@ -131,6 +97,51 @@ module ActiveRecord
|
||||||
arel = arel.join(join)
|
arel = arel.join(join)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
arel.joins(arel)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_arel
|
||||||
|
arel = table
|
||||||
|
|
||||||
|
joined_associations = []
|
||||||
|
association_joins = []
|
||||||
|
|
||||||
|
joins = @joins_values.map {|j| j.respond_to?(:strip) ? j.strip : j}.uniq
|
||||||
|
|
||||||
|
joins.each do |join|
|
||||||
|
association_joins << join if [Hash, Array, Symbol].include?(join.class) && !array_of_strings?(join)
|
||||||
|
end
|
||||||
|
|
||||||
|
stashed_association_joins = joins.select {|j| j.is_a?(ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation)}
|
||||||
|
|
||||||
|
non_association_joins = (joins - association_joins - stashed_association_joins).reject {|j| j.blank?}
|
||||||
|
custom_joins = custom_join_sql(*non_association_joins)
|
||||||
|
|
||||||
|
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins, custom_joins)
|
||||||
|
|
||||||
|
join_dependency.graft(*stashed_association_joins)
|
||||||
|
|
||||||
|
@implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
|
||||||
|
|
||||||
|
to_join = []
|
||||||
|
|
||||||
|
join_dependency.join_associations.each do |association|
|
||||||
|
if (association_relation = association.relation).is_a?(Array)
|
||||||
|
to_join << [association_relation.first, association.join_class, association.association_join.first]
|
||||||
|
to_join << [association_relation.last, association.join_class, association.association_join.last]
|
||||||
|
else
|
||||||
|
to_join << [association_relation, association.join_class, association.association_join]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
to_join.each do |tj|
|
||||||
|
unless joined_associations.detect {|ja| ja[0] == tj[0] && ja[1] == tj[1] && ja[2] == tj[2] }
|
||||||
|
joined_associations << tj
|
||||||
|
arel = arel.join(tj[0], tj[1]).on(*tj[2])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
arel = arel.join(custom_joins)
|
||||||
|
|
||||||
@where_values.uniq.each do |where|
|
@where_values.uniq.each do |where|
|
||||||
next if where.blank?
|
next if where.blank?
|
||||||
|
|
|
@ -29,6 +29,15 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
|
||||||
assert_equal 2, authors[1].categorizations.size
|
assert_equal 2, authors[1].categorizations.size
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations
|
||||||
|
assert_nothing_raised do
|
||||||
|
Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).all
|
||||||
|
end
|
||||||
|
authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).all
|
||||||
|
assert_equal 1, assert_no_queries { authors.size }
|
||||||
|
assert_equal 9, assert_no_queries { authors[0].comments.size }
|
||||||
|
end
|
||||||
|
|
||||||
def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations
|
def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations
|
||||||
authors = Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id")
|
authors = Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id")
|
||||||
assert_equal 2, authors.size
|
assert_equal 2, authors.size
|
||||||
|
|
Loading…
Reference in a new issue