diff options
Diffstat (limited to 'activerecord/lib/active_record')
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 |