aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record')
-rw-r--r--activerecord/lib/active_record/associations.rb385
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb73
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb1
-rw-r--r--activerecord/lib/active_record/associations/nested_has_many_through.rb158
-rw-r--r--activerecord/lib/active_record/associations/through_association_scope.rb200
-rw-r--r--activerecord/lib/active_record/reflection.rb51
6 files changed, 634 insertions, 234 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index affa2fbcaf..2a72fa95c9 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -111,8 +111,10 @@ module ActiveRecord
autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association'
autoload :HasManyAssociation, 'active_record/associations/has_many_association'
autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association'
+ autoload :NestedHasManyThroughAssociation, 'active_record/associations/nested_has_many_through_association'
autoload :HasOneAssociation, 'active_record/associations/has_one_association'
autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association'
+ autoload :AliasTracker, 'active_record/associations/alias_tracker'
# Clears out the association cache.
def clear_association_cache #:nodoc:
@@ -1833,7 +1835,7 @@ module ActiveRecord
end
class JoinDependency # :nodoc:
- attr_reader :join_parts, :reflections, :table_aliases
+ attr_reader :join_parts, :reflections, :alias_tracker
def initialize(base, associations, joins)
@join_parts = [JoinBase.new(base, joins)]
@@ -1841,8 +1843,8 @@ module ActiveRecord
@reflections = []
@base_records_hash = {}
@base_records_in_order = []
- @table_aliases = Hash.new(0)
- @table_aliases[base.table_name] = 1
+ @alias_tracker = AliasTracker.new(joins)
+ @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
build(associations)
end
@@ -1862,17 +1864,6 @@ module ActiveRecord
join_parts.first
end
- def count_aliases_from_table_joins(name)
- # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
- quoted_name = join_base.active_record.connection.quote_table_name(name.downcase).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)
rows.each_with_index do |row, i|
primary_id = join_base.record_id(row)
@@ -2125,11 +2116,11 @@ module ActiveRecord
# 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, :aliased_table_name
+ attr_reader :aliased_prefix
- delegate :options, :through_reflection, :source_reflection, :to => :reflection
+ delegate :options, :through_reflection, :source_reflection, :through_reflection_chain, :to => :reflection
delegate :table, :table_name, :to => :parent, :prefix => true
+ delegate :alias_tracker, :to => :join_dependency
def initialize(reflection, join_dependency, parent = nil)
reflection.check_validity!
@@ -2140,15 +2131,13 @@ module ActiveRecord
super(reflection.klass)
- @reflection = reflection
- @join_dependency = join_dependency
- @parent = parent
- @join_type = Arel::InnerJoin
+ @reflection = reflection
+ @join_dependency = join_dependency
+ @parent = parent
+ @join_type = Arel::InnerJoin
+ @aliased_prefix = "t#{ join_dependency.join_parts.size }"
- # This must be done eagerly upon initialisation because the alias which is produced
- # depends on the state of the join dependency, but we want it to work the same way
- # every time.
- allocate_aliases
+ setup_tables
end
def ==(other)
@@ -2164,7 +2153,91 @@ module ActiveRecord
end
def join_to(relation)
- send("join_#{reflection.macro}_to", relation)
+ # The chain starts with the target table, but we want to end with it here (makes
+ # more sense in this context)
+ chain = through_reflection_chain.reverse
+
+ foreign_table = parent_table
+
+ chain.zip(@tables).each do |reflection, table|
+ conditions = []
+
+ if reflection.source_reflection.nil?
+ case reflection.macro
+ when :belongs_to
+ key = reflection.options[:primary_key] ||
+ reflection.klass.primary_key
+ foreign_key = reflection.primary_key_name
+ when :has_many, :has_one
+ key = reflection.primary_key_name
+ foreign_key = reflection.options[:primary_key] ||
+ reflection.active_record.primary_key
+
+ conditions << polymorphic_conditions(reflection, table)
+ when :has_and_belongs_to_many
+ # For habtm, we need to deal with the join table at the same time as the
+ # target table (because unlike a :through association, there is no reflection
+ # to represent the join table)
+ table, join_table = table
+
+ # TODO: Can join_key just be reflection.primary_key_name ?
+ join_key = reflection.options[:foreign_key] ||
+ reflection.active_record.to_s.foreign_key
+ join_foreign_key = reflection.active_record.primary_key
+
+ relation = relation.join(join_table, join_type).on(
+ join_table[join_key].
+ eq(foreign_table[join_foreign_key])
+ )
+
+ # We've done the first join now, so update the foreign_table for the second
+ foreign_table = join_table
+
+ # TODO: Can foreign_key be reflection.association_foreign_key?
+ key = reflection.klass.primary_key
+ foreign_key = reflection.options[:association_foreign_key] ||
+ reflection.klass.to_s.foreign_key
+ end
+ else
+ case reflection.source_reflection.macro
+ when :belongs_to
+ key = reflection.klass.primary_key
+ foreign_key = reflection.source_reflection.primary_key_name
+
+ conditions << source_type_conditions(reflection, foreign_table)
+ when :has_many, :has_one
+ key = reflection.source_reflection.primary_key_name
+ foreign_key = reflection.source_reflection.klass.primary_key
+ when :has_and_belongs_to_many
+ table, join_table = table
+
+ join_key = reflection.source_reflection.primary_key_name
+ join_foreign_key = reflection.source_reflection.klass.primary_key
+
+ relation = relation.join(join_table, join_type).on(
+ join_table[join_key].
+ eq(foreign_table[join_foreign_key])
+ )
+
+ foreign_table = join_table
+
+ key = reflection.klass.primary_key
+ foreign_key = reflection.source_reflection.association_foreign_key
+ end
+ end
+
+ conditions << table[key].eq(foreign_table[foreign_key])
+
+ conditions << reflection_conditions(reflection, table)
+ conditions << sti_conditions(reflection, table)
+
+ relation = relation.join(table, join_type).on(*conditions.compact)
+
+ # The current table in this iteration becomes the foreign table in the next
+ foreign_table = table
+ end
+
+ relation
end
def join_relation(joining_relation)
@@ -2173,213 +2246,117 @@ module ActiveRecord
end
def table
- @table ||= Arel::Table.new(
- table_name, :as => aliased_table_name,
- :engine => arel_engine, :columns => active_record.columns
- )
+ if reflection.macro == :has_and_belongs_to_many
+ @tables.last.first
+ else
+ @tables.last
+ end
+ end
+
+ def aliased_table_name
+ table.table_alias || table.name
end
- # More semantic name given we are talking about associations
- alias_method :target_table, :table
-
protected
- def aliased_table_name_for(name, suffix = nil)
- if @join_dependency.table_aliases[name].zero?
- @join_dependency.table_aliases[name] = @join_dependency.count_aliases_from_table_joins(name)
- end
-
- if !@join_dependency.table_aliases[name].zero? # We need an alias
- name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
- @join_dependency.table_aliases[name] += 1
- 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
- @join_dependency.table_aliases[name] += 1
- end
-
+ def table_alias_for(reflection, join = false)
+ name = alias_tracker.pluralize(reflection.name)
+ name << "_#{parent_table_name}"
+ name << "_join" if join
name
end
- def pluralize(table_name)
- ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
- end
-
def interpolate_sql(sql)
instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__)
end
private
- def allocate_aliases
- @aliased_prefix = "t#{ join_dependency.join_parts.size }"
- @aliased_table_name = aliased_table_name_for(table_name)
-
- if reflection.macro == :has_and_belongs_to_many
- @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
- elsif [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
- @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
- end
- end
-
- def process_conditions(conditions, table_name)
- Arel.sql(interpolate_sql(sanitize_sql(conditions, table_name)))
- end
-
- def join_target_table(relation, *conditions)
- relation = relation.join(target_table, join_type)
-
- # If the target table is an STI model then we must be sure to only include records of
- # its type and its sub-types.
- unless active_record.descends_from_active_record?
- sti_column = target_table[active_record.inheritance_column]
+ # Generate aliases and Arel::Table instances for each of the tables which we will
+ # later generate joins for. We must do this in advance in order to correctly allocate
+ # the proper alias.
+ def setup_tables
+ @tables = through_reflection_chain.map do |reflection|
+ aliased_table_name = alias_tracker.aliased_name_for(
+ reflection.table_name,
+ table_alias_for(reflection, reflection != self.reflection)
+ )
- sti_condition = sti_column.eq(active_record.sti_name)
- active_record.descendants.each do |subclass|
- sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name))
- end
+ table = Arel::Table.new(
+ reflection.table_name, :engine => arel_engine,
+ :as => aliased_table_name, :columns => reflection.klass.columns
+ )
- conditions << sti_condition
+ # For habtm, we have two Arel::Table instances related to a single reflection, so
+ # we just store them as a pair in the array.
+ if reflection.macro == :has_and_belongs_to_many ||
+ (reflection.source_reflection &&
+ reflection.source_reflection.macro == :has_and_belongs_to_many)
+
+ join_table_name = (reflection.source_reflection || reflection).options[:join_table]
+
+ aliased_join_table_name = alias_tracker.aliased_name_for(
+ join_table_name,
+ table_alias_for(reflection, true)
+ )
+
+ join_table = Arel::Table.new(
+ join_table_name, :engine => arel_engine,
+ :as => aliased_join_table_name
+ )
+
+ [table, join_table]
+ else
+ table
+ end
end
- # If the reflection has conditions, add them
- if options[:conditions]
- conditions << process_conditions(options[:conditions], aliased_table_name)
- end
+ # The joins are generated from the through_reflection_chain in reverse order, so
+ # reverse the tables too (but it's important to generate the aliases in the 'forward'
+ # order, which is why we only do the reversal now.
+ @tables.reverse!
- relation = relation.on(*conditions)
+ @tables
end
-
- def join_has_and_belongs_to_many_to(relation)
- join_table = Arel::Table.new(
- options[:join_table], :engine => arel_engine,
- :as => @aliased_join_table_name
- )
-
- fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key
- klass_fk = options[:association_foreign_key] || reflection.klass.to_s.foreign_key
-
- relation = relation.join(join_table, join_type)
- relation = relation.on(
- join_table[fk].
- eq(parent_table[reflection.active_record.primary_key])
- )
-
- join_target_table(
- relation,
- target_table[reflection.klass.primary_key].
- eq(join_table[klass_fk])
- )
- end
-
- def join_has_many_to(relation)
- if reflection.options[:through]
- join_has_many_through_to(relation)
- elsif reflection.options[:as]
- join_has_many_polymorphic_to(relation)
- else
- foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
- primary_key = options[:primary_key] || parent.primary_key
-
- join_target_table(
- relation,
- target_table[foreign_key].
- eq(parent_table[primary_key])
- )
+
+ def reflection_conditions(reflection, table)
+ if reflection.options[:conditions]
+ Arel.sql(interpolate_sql(sanitize_sql(
+ reflection.options[:conditions],
+ table.table_alias || table.name
+ )))
end
end
- alias :join_has_one_to :join_has_many_to
- def join_has_many_through_to(relation)
- join_table = Arel::Table.new(
- through_reflection.klass.table_name, :engine => arel_engine,
- :as => @aliased_join_table_name
- )
-
- jt_conditions = []
- jt_foreign_key = first_key = second_key = nil
-
- if through_reflection.options[:as] # has_many :through against a polymorphic join
- as_key = through_reflection.options[:as].to_s
- jt_foreign_key = as_key + '_id'
+ def sti_conditions(reflection, table)
+ unless reflection.klass.descends_from_active_record?
+ sti_column = table[reflection.klass.inheritance_column]
- jt_conditions <<
- join_table[as_key + '_type'].
- eq(parent.active_record.base_class.name)
- else
- jt_foreign_key = through_reflection.primary_key_name
- end
-
- case source_reflection.macro
- when :has_many
- second_key = options[:foreign_key] || primary_key
-
- if source_reflection.options[:as]
- first_key = "#{source_reflection.options[:as]}_id"
- else
- first_key = through_reflection.klass.base_class.to_s.foreign_key
- end
-
- unless through_reflection.klass.descends_from_active_record?
- jt_conditions <<
- join_table[through_reflection.active_record.inheritance_column].
- eq(through_reflection.klass.sti_name)
- end
- when :belongs_to
- first_key = primary_key
+ condition = sti_column.eq(reflection.klass.sti_name)
- if reflection.options[:source_type]
- second_key = source_reflection.association_foreign_key
-
- jt_conditions <<
- join_table[reflection.source_reflection.options[:foreign_type]].
- eq(reflection.options[:source_type])
- else
- second_key = source_reflection.primary_key_name
+ reflection.klass.descendants.each do |subclass|
+ condition = condition.or(sti_column.eq(subclass.sti_name))
end
+
+ condition
end
-
- jt_conditions <<
- parent_table[parent.primary_key].
- eq(join_table[jt_foreign_key])
-
- if through_reflection.options[:conditions]
- jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name)
- end
-
- relation = relation.join(join_table, join_type).on(*jt_conditions)
-
- join_target_table(
- relation,
- target_table[first_key].eq(join_table[second_key])
- )
end
- def join_has_many_polymorphic_to(relation)
- join_target_table(
- relation,
- target_table["#{reflection.options[:as]}_id"].
- eq(parent_table[parent.primary_key]),
- target_table["#{reflection.options[:as]}_type"].
- eq(parent.active_record.base_class.name)
- )
- end
-
- def join_belongs_to_to(relation)
- foreign_key = options[:foreign_key] || reflection.primary_key_name
- primary_key = options[:primary_key] || reflection.klass.primary_key
-
- join_target_table(
- relation,
- target_table[primary_key].eq(parent_table[foreign_key])
- )
- end
- end
+ def source_type_conditions(reflection, foreign_table)
+ if reflection.options[:source_type]
+ foreign_table[reflection.source_reflection.options[:foreign_type]].
+ eq(reflection.options[:source_type])
+ end
+ end
+
+ def polymorphic_conditions(reflection, table)
+ if reflection.options[:as]
+ table["#{reflection.options[:as]}_type"].
+ eq(reflection.active_record.base_class.name)
+ end
+ end
end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
new file mode 100644
index 0000000000..10e90ec117
--- /dev/null
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -0,0 +1,73 @@
+require 'active_support/core_ext/string/conversions'
+
+module ActiveRecord
+ module Associations
+ # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and
+ # ActiveRecord::Associations::ThroughAssociationScope
+ class AliasTracker # :nodoc:
+ # other_sql is some other sql which might conflict with the aliases we assign here. Therefore
+ # we store other_sql so that we can scan it before assigning a specific name.
+ def initialize(other_sql = nil)
+ @aliases = Hash.new
+ @other_sql = other_sql.to_s.downcase
+ end
+
+ def aliased_name_for(table_name, aliased_name = nil)
+ aliased_name ||= table_name
+
+ initialize_count_for(table_name) if @aliases[table_name].nil?
+
+ if @aliases[table_name].zero?
+ # If it's zero, we can have our table_name
+ @aliases[table_name] = 1
+ table_name
+ else
+ # Otherwise, we need to use an alias
+ aliased_name = connection.table_alias_for(aliased_name)
+
+ initialize_count_for(aliased_name) if @aliases[aliased_name].nil?
+
+ # Update the count
+ @aliases[aliased_name] += 1
+
+ if @aliases[aliased_name] > 1
+ "#{truncate(aliased_name)}_#{@aliases[aliased_name]}"
+ else
+ aliased_name
+ end
+ end
+ end
+
+ def pluralize(table_name)
+ ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
+ end
+
+ private
+
+ def initialize_count_for(name)
+ @aliases[name] = 0
+
+ unless @other_sql.blank?
+ # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
+ quoted_name = connection.quote_table_name(name.downcase).downcase
+
+ # Table names
+ @aliases[name] += @other_sql.scan(/join(?:\s+\w+)?\s+#{quoted_name}\son/).size
+
+ # Table aliases
+ @aliases[name] += @other_sql.scan(/join(?:\s+\w+)?\s+\S+\s+#{quoted_name}\son/).size
+ end
+
+ @aliases[name]
+ end
+
+ def truncate(name)
+ name[0..connection.table_alias_length-3]
+ end
+
+ def connection
+ ActiveRecord::Base.connection
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index 97883d8393..313d9da621 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -1,4 +1,5 @@
require "active_record/associations/through_association_scope"
+require "active_record/associations/nested_has_many_through"
require 'active_support/core_ext/object/blank'
module ActiveRecord
diff --git a/activerecord/lib/active_record/associations/nested_has_many_through.rb b/activerecord/lib/active_record/associations/nested_has_many_through.rb
new file mode 100644
index 0000000000..d699a60edb
--- /dev/null
+++ b/activerecord/lib/active_record/associations/nested_has_many_through.rb
@@ -0,0 +1,158 @@
+# TODO: Remove in the end, when its functionality is fully integrated in ThroughAssociationScope.
+
+module ActiveRecord
+ module Associations
+ module NestedHasManyThrough
+ def self.included(klass)
+ klass.alias_method_chain :construct_conditions, :nesting
+ klass.alias_method_chain :construct_joins, :nesting
+ end
+
+ def construct_joins_with_nesting(custom_joins = nil)
+ if nested?
+ @nested_join_attributes ||= construct_nested_join_attributes
+ "#{construct_nested_join_attributes[:joins]} #{@reflection.options[:joins]} #{custom_joins}"
+ else
+ construct_joins_without_nesting(custom_joins)
+ end
+ end
+
+ def construct_conditions_with_nesting
+ if nested?
+ @nested_join_attributes ||= construct_nested_join_attributes
+ if @reflection.through_reflection && @reflection.through_reflection.macro == :belongs_to
+ "#{@nested_join_attributes[:remote_key]} = #{belongs_to_quoted_key} #{@nested_join_attributes[:conditions]}"
+ else
+ "#{@nested_join_attributes[:remote_key]} = #{@owner.quoted_id} #{@nested_join_attributes[:conditions]}"
+ end
+ else
+ construct_conditions_without_nesting
+ end
+ end
+
+ protected
+
+ # Given any belongs_to or has_many (including has_many :through) association,
+ # return the essential components of a join corresponding to that association, namely:
+ #
+ # * <tt>:joins</tt>: any additional joins required to get from the association's table
+ # (reflection.table_name) to the table that's actually joining to the active record's table
+ # * <tt>:remote_key</tt>: the name of the key in the join table (qualified by table name) which will join
+ # to a field of the active record's table
+ # * <tt>:local_key</tt>: the name of the key in the local table (not qualified by table name) which will
+ # take part in the join
+ # * <tt>:conditions</tt>: any additional conditions (e.g. filtering by type for a polymorphic association,
+ # or a :conditions clause explicitly given in the association), including a leading AND
+ def construct_nested_join_attributes(reflection = @reflection, association_class = reflection.klass, table_ids = {association_class.table_name => 1})
+ if (reflection.macro == :has_many || reflection.macro == :has_one) && reflection.through_reflection
+ construct_has_many_through_attributes(reflection, table_ids)
+ else
+ construct_has_many_or_belongs_to_attributes(reflection, association_class, table_ids)
+ end
+ end
+
+ def construct_has_many_through_attributes(reflection, table_ids)
+ # Construct the join components of the source association, so that we have a path from
+ # the eventual target table of the association up to the table named in :through, and
+ # all tables involved are allocated table IDs.
+ source_attrs = construct_nested_join_attributes(reflection.source_reflection, reflection.klass, table_ids)
+
+ # Determine the alias of the :through table; this will be the last table assigned
+ # when constructing the source join components above.
+ through_table_alias = through_table_name = reflection.through_reflection.table_name
+ through_table_alias += "_#{table_ids[through_table_name]}" unless table_ids[through_table_name] == 1
+
+ # Construct the join components of the through association, so that we have a path to
+ # the active record's table.
+ through_attrs = construct_nested_join_attributes(reflection.through_reflection, reflection.through_reflection.klass, table_ids)
+
+ # Any subsequent joins / filters on owner attributes will act on the through association,
+ # so that's what we return for the conditions/keys of the overall association.
+ conditions = through_attrs[:conditions]
+ conditions += " AND #{interpolate_sql(reflection.klass.send(:sanitize_sql, reflection.options[:conditions]))}" if reflection.options[:conditions]
+
+ {
+ :joins => "%s INNER JOIN %s ON ( %s = %s.%s %s) %s %s" % [
+ source_attrs[:joins],
+ through_table_name == through_table_alias ? through_table_name : "#{through_table_name} #{through_table_alias}",
+ source_attrs[:remote_key],
+ through_table_alias, source_attrs[:local_key],
+ source_attrs[:conditions],
+ through_attrs[:joins],
+ reflection.options[:joins]
+ ],
+ :remote_key => through_attrs[:remote_key],
+ :local_key => through_attrs[:local_key],
+ :conditions => conditions
+ }
+ end
+
+ # reflection is not has_many :through; it's a standard has_many / belongs_to instead
+ # TODO: see if we can defer to rails code here a bit more
+ def construct_has_many_or_belongs_to_attributes(reflection, association_class, table_ids)
+ # Determine the alias used for remote_table_name, if any. In all cases this will already
+ # have been assigned an ID in table_ids (either through being involved in a previous join,
+ # or - if it's the first table in the query - as the default value of table_ids)
+ remote_table_alias = remote_table_name = association_class.table_name
+ remote_table_alias += "_#{table_ids[remote_table_name]}" unless table_ids[remote_table_name] == 1
+
+ # Assign a new alias for the local table.
+ local_table_alias = local_table_name = reflection.active_record.table_name
+ if table_ids[local_table_name]
+ table_id = table_ids[local_table_name] += 1
+ local_table_alias += "_#{table_id}"
+ else
+ table_ids[local_table_name] = 1
+ end
+
+ conditions = ''
+ # Add type_condition, if applicable
+ conditions += " AND #{association_class.send(:type_condition).to_sql}" if association_class.finder_needs_type_condition?
+ # Add custom conditions
+ conditions += " AND (#{interpolate_sql(association_class.send(:sanitize_sql, reflection.options[:conditions]))})" if reflection.options[:conditions]
+
+ if reflection.macro == :belongs_to
+ if reflection.options[:polymorphic]
+ conditions += " AND #{local_table_alias}.#{reflection.options[:foreign_type]} = #{reflection.active_record.quote_value(association_class.base_class.name.to_s)}"
+ end
+ {
+ :joins => reflection.options[:joins],
+ :remote_key => "#{remote_table_alias}.#{association_class.primary_key}",
+ :local_key => reflection.primary_key_name,
+ :conditions => conditions
+ }
+ else
+ # Association is has_many (without :through)
+ if reflection.options[:as]
+ conditions += " AND #{remote_table_alias}.#{reflection.options[:as]}_type = #{reflection.active_record.quote_value(reflection.active_record.base_class.name.to_s)}"
+ end
+ {
+ :joins => "#{reflection.options[:joins]}",
+ :remote_key => "#{remote_table_alias}.#{reflection.primary_key_name}",
+ :local_key => reflection.klass.primary_key,
+ :conditions => conditions
+ }
+ end
+ end
+
+ def belongs_to_quoted_key
+ attribute = @reflection.through_reflection.primary_key_name
+ column = @owner.column_for_attribute attribute
+
+ @owner.send(:quote_value, @owner.send(attribute), column)
+ end
+
+ def nested?
+ through_source_reflection? || through_through_reflection?
+ end
+
+ def through_source_reflection?
+ @reflection.source_reflection && @reflection.source_reflection.options[:through]
+ end
+
+ def through_through_reflection?
+ @reflection.through_reflection && @reflection.through_reflection.options[:through]
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb
index cabb33c4a8..582474355e 100644
--- a/activerecord/lib/active_record/associations/through_association_scope.rb
+++ b/activerecord/lib/active_record/associations/through_association_scope.rb
@@ -1,3 +1,5 @@
+require 'enumerator'
+
module ActiveRecord
# = Active Record Through Association Scope
module Associations
@@ -18,10 +20,18 @@ module ActiveRecord
end
# Build SQL conditions from attributes, qualified by table name.
+ # TODO: Conditions on joins
def construct_conditions
- table_name = @reflection.through_reflection.quoted_table_name
- conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value|
- "#{table_name}.#{attr} = #{value}"
+ reflection = @reflection.through_reflection_chain.last
+
+ if reflection.macro == :has_and_belongs_to_many
+ table_alias = table_aliases[reflection].first
+ else
+ table_alias = table_aliases[reflection]
+ end
+
+ conditions = construct_quoted_owner_attributes(reflection).map do |attr, value|
+ "#{table_alias}.#{attr} = #{value}"
end
conditions << sql_conditions if sql_conditions
"(" + conditions.join(') AND (') + ")"
@@ -49,36 +59,172 @@ module ActiveRecord
distinct = "DISTINCT " if @reflection.options[:uniq]
selected = custom_select || @reflection.options[:select] || "#{distinct}#{@reflection.quoted_table_name}.*"
end
-
+
def construct_joins(custom_joins = nil)
- polymorphic_join = nil
- if @reflection.source_reflection.macro == :belongs_to
- reflection_primary_key = @reflection.klass.primary_key
- source_primary_key = @reflection.source_reflection.primary_key_name
- if @reflection.options[:source_type]
- polymorphic_join = "AND %s.%s = %s" % [
- @reflection.through_reflection.quoted_table_name, "#{@reflection.source_reflection.options[:foreign_type]}",
- @owner.class.quote_value(@reflection.options[:source_type])
- ]
- end
- else
- reflection_primary_key = @reflection.source_reflection.primary_key_name
- source_primary_key = @reflection.through_reflection.klass.primary_key
- if @reflection.source_reflection.options[:as]
- polymorphic_join = "AND %s.%s = %s" % [
- @reflection.quoted_table_name, "#{@reflection.source_reflection.options[:as]}_type",
- @owner.class.quote_value(@reflection.through_reflection.klass.name)
- ]
+ # p @reflection.through_reflection_chain
+
+ "#{construct_through_joins} #{@reflection.options[:joins]} #{custom_joins}"
+ end
+
+ def construct_through_joins
+ joins = []
+
+ # Iterate over each pair in the through reflection chain, joining them together
+ @reflection.through_reflection_chain.each_cons(2) do |left, right|
+ right_table_and_alias = table_name_and_alias(right.quoted_table_name, table_aliases[right])
+
+ if left.source_reflection.nil?
+ # TODO: Perhaps need to pay attention to left.options[:primary_key] and
+ # left.options[:foreign_key] in places here
+
+ case left.macro
+ when :belongs_to
+ joins << inner_join_sql(
+ right_table_and_alias,
+ table_aliases[left], left.klass.primary_key,
+ table_aliases[right], left.primary_key_name
+ )
+ when :has_many, :has_one
+ joins << inner_join_sql(
+ right_table_and_alias,
+ table_aliases[left], left.primary_key_name,
+ table_aliases[right], right.klass.primary_key,
+ polymorphic_conditions(left, left.options[:as])
+ )
+ when :has_and_belongs_to_many
+ raise NotImplementedError
+ end
+ else
+ case left.source_reflection.macro
+ when :belongs_to
+ joins << inner_join_sql(
+ right_table_and_alias,
+ table_aliases[left], left.klass.primary_key,
+ table_aliases[right], left.source_reflection.primary_key_name,
+ source_type_conditions(left)
+ )
+ when :has_many, :has_one
+ if right.macro == :has_and_belongs_to_many
+ join_table, right_table = table_aliases[right]
+ right_table_and_alias = table_name_and_alias(right.quoted_table_name, right_table)
+ else
+ right_table = table_aliases[right]
+ end
+
+ joins << inner_join_sql(
+ right_table_and_alias,
+ table_aliases[left], left.source_reflection.primary_key_name,
+ right_table, right.klass.primary_key,
+ polymorphic_conditions(left, left.source_reflection.options[:as])
+ )
+
+ if right.macro == :has_and_belongs_to_many
+ joins << inner_join_sql(
+ table_name_and_alias(
+ quote_table_name(right.options[:join_table]),
+ join_table
+ ),
+ right_table, right.klass.primary_key,
+ join_table, right.association_foreign_key
+ )
+ end
+ when :has_and_belongs_to_many
+ join_table, left_table = table_aliases[left]
+
+ joins << inner_join_sql(
+ table_name_and_alias(
+ quote_table_name(left.source_reflection.options[:join_table]),
+ join_table
+ ),
+ left_table, left.klass.primary_key,
+ join_table, left.source_reflection.association_foreign_key
+ )
+
+ joins << inner_join_sql(
+ right_table_and_alias,
+ join_table, left.source_reflection.primary_key_name,
+ table_aliases[right], right.klass.primary_key
+ )
+ end
end
end
+
+ joins.join(" ")
+ end
+
+ def alias_tracker
+ @alias_tracker ||= AliasTracker.new
+ end
- "INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [
- @reflection.through_reflection.quoted_table_name,
- @reflection.quoted_table_name, reflection_primary_key,
- @reflection.through_reflection.quoted_table_name, source_primary_key,
- polymorphic_join
+ def table_aliases
+ @table_aliases ||= begin
+ @reflection.through_reflection_chain.inject({}) do |aliases, reflection|
+ table_alias = quote_table_name(alias_tracker.aliased_name_for(
+ reflection.table_name,
+ table_alias_for(reflection, reflection != @reflection)
+ ))
+
+ if reflection.macro == :has_and_belongs_to_many ||
+ (reflection.source_reflection &&
+ reflection.source_reflection.macro == :has_and_belongs_to_many)
+
+ join_table_alias = quote_table_name(alias_tracker.aliased_name_for(
+ (reflection.source_reflection || reflection).options[:join_table],
+ table_alias_for(reflection, true)
+ ))
+
+ aliases[reflection] = [join_table_alias, table_alias]
+ else
+ aliases[reflection] = table_alias
+ end
+
+ aliases
+ end
+ end
+ end
+
+ def table_alias_for(reflection, join = false)
+ name = alias_tracker.pluralize(reflection.name)
+ name << "_#{@reflection.name}"
+ name << "_join" if join
+ name
+ end
+
+ def quote_table_name(table_name)
+ @reflection.klass.connection.quote_table_name(table_name)
+ end
+
+ def table_name_and_alias(table_name, table_alias)
+ "#{table_name} #{table_alias if table_alias != table_name}".strip
+ end
+
+ def inner_join_sql(table, on_left_table, on_left_key, on_right_table, on_right_key, conds = nil)
+ "INNER JOIN %s ON %s.%s = %s.%s %s" % [
+ table,
+ on_left_table, on_left_key,
+ on_right_table, on_right_key,
+ conds
]
end
+
+ def polymorphic_conditions(reflection, interface_name)
+ if interface_name
+ "AND %s.%s = %s" % [
+ table_aliases[reflection], "#{interface_name}_type",
+ @owner.class.quote_value(reflection.active_record.base_class.name)
+ ]
+ end
+ end
+
+ def source_type_conditions(reflection)
+ if reflection.options[:source_type]
+ "AND %s.%s = %s" % [
+ table_aliases[reflection.through_reflection],
+ reflection.source_reflection.options[:foreign_type].to_s,
+ @owner.class.quote_value(reflection.options[:source_type])
+ ]
+ end
+ end
# Construct attributes for associate pointing to owner.
def construct_owner_attributes(reflection)
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index db18fb7c0f..b7cd466e13 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -131,6 +131,14 @@ module ActiveRecord
@sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
end
+ # TODO: Remove these in the final patch. I am just using them for debugging etc.
+ def inspect
+ "#<#{code_name}>"
+ end
+ def code_name
+ "#{active_record.name}.#{macro} :#{name}"
+ end
+
private
def derive_class_name
name.to_s.camelize
@@ -241,6 +249,10 @@ module ActiveRecord
def through_reflection
false
end
+
+ def through_reflection_chain
+ [self]
+ end
def through_reflection_primary_key_name
end
@@ -304,6 +316,16 @@ module ActiveRecord
def belongs_to?
macro == :belongs_to
end
+
+ # TODO: Remove for final patch. Just here for debugging.
+ def inspect
+ str = "#<#{code_name}, @source_reflection="
+ str << (source_reflection.respond_to?(:code_name) ? source_reflection.code_name : source_reflection.inspect)
+ str << ", @through_reflection="
+ str << (through_reflection.respond_to?(:code_name) ? through_reflection.code_name : through_reflection.inspect)
+ str << ">"
+ str
+ end
private
def derive_class_name
@@ -352,6 +374,27 @@ module ActiveRecord
def through_reflection
@through_reflection ||= active_record.reflect_on_association(options[:through])
end
+
+ # TODO: Documentation
+ def through_reflection_chain
+ @through_reflection_chain ||= begin
+ if source_reflection.through_reflection
+ # If the source reflection goes through another reflection, then the chain must start
+ # by getting us to the source reflection.
+ chain = source_reflection.through_reflection_chain
+ else
+ # If the source reflection does not go through another reflection, then we can get
+ # to this reflection directly, and so start the chain here
+ chain = [self]
+ end
+
+ # Recursively build the rest of the chain
+ chain += through_reflection.through_reflection_chain
+
+ # Finally return the completed chain
+ chain
+ end
+ end
# Gets an array of possible <tt>:through</tt> source reflection names:
#
@@ -378,9 +421,11 @@ module ActiveRecord
raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection)
end
- unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil?
- raise HasManyThroughSourceAssociationMacroError.new(self)
- end
+ # TODO: Presumably remove the HasManyThroughSourceAssociationMacroError class and delete these lines.
+ # Think about whether there are any cases which should still be disallowed.
+ # unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil?
+ # raise HasManyThroughSourceAssociationMacroError.new(self)
+ # end
check_validity_of_inverse!
end