diff options
Diffstat (limited to 'activerecord/lib/active_record')
7 files changed, 602 insertions, 227 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 565ebf8197..67c204f154 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -111,6 +111,7 @@ 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' @@ -1833,10 +1834,10 @@ module ActiveRecord end class JoinDependency # :nodoc: - attr_reader :joins, :reflections, :table_aliases + attr_reader :join_parts, :reflections, :table_aliases def initialize(base, associations, joins) - @joins = [JoinBase.new(base, joins)] + @join_parts = [JoinBase.new(base, joins)] @associations = associations @reflections = [] @base_records_hash = {} @@ -1849,17 +1850,17 @@ module ActiveRecord def graft(*associations) associations.each do |association| join_associations.detect {|a| association == a} || - build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_class) + build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type) end self end def join_associations - @joins.last(@joins.length - 1) + join_parts.last(join_parts.length - 1) end def join_base - @joins[0] + join_parts.first end def count_aliases_from_table_joins(name) @@ -1917,22 +1918,24 @@ module ActiveRecord protected - def build(associations, parent = nil, join_class = Arel::InnerJoin) - parent ||= @joins.last + def build(associations, parent = nil, join_type = Arel::InnerJoin) + parent ||= join_parts.last case associations when Symbol, String reflection = parent.reflections[associations.to_s.intern] or raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" @reflections << reflection - @joins << build_join_association(reflection, parent).with_join_class(join_class) + join_association = build_join_association(reflection, parent) + join_association.join_type = join_type + @join_parts << join_association when Array associations.each do |association| - build(association, parent, join_class) + build(association, parent, join_type) end when Hash associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name| - build(name, parent, join_class) - build(associations[name], nil, join_class) + build(name, parent, join_type) + build(associations[name], nil, join_type) end else raise ConfigurationError, associations.inspect @@ -1949,91 +1952,111 @@ module ActiveRecord JoinAssociation.new(reflection, self, parent) end - def construct(parent, associations, joins, row) + def construct(parent, associations, join_parts, row) case associations when Symbol, String - join = joins.detect{|j| j.reflection.name.to_s == associations.to_s && j.parent_table_name == parent.class.table_name } - raise(ConfigurationError, "No such association") if join.nil? + join_part = join_parts.detect { |j| + j.reflection.name.to_s == associations.to_s && + j.parent_table_name == parent.class.table_name } + raise(ConfigurationError, "No such association") if join_part.nil? - joins.delete(join) - construct_association(parent, join, row) + join_parts.delete(join_part) + construct_association(parent, join_part, row) when Array associations.each do |association| - construct(parent, association, joins, row) + construct(parent, association, join_parts, row) end when Hash associations.sort_by { |k,_| k.to_s }.each do |name, assoc| - join = joins.detect{|j| j.reflection.name.to_s == name.to_s && j.parent_table_name == parent.class.table_name } - raise(ConfigurationError, "No such association") if join.nil? - - association = construct_association(parent, join, row) - joins.delete(join) - construct(association, assoc, joins, row) if association + join_part = join_parts.detect{ |j| + j.reflection.name.to_s == name.to_s && + j.parent_table_name == parent.class.table_name } + raise(ConfigurationError, "No such association") if join_part.nil? + + association = construct_association(parent, join_part, row) + join_parts.delete(join_part) + construct(association, assoc, join_parts, row) if association end else raise ConfigurationError, associations.inspect end end - def construct_association(record, join, row) - return if record.id.to_s != join.parent.record_id(row).to_s + def construct_association(record, join_part, row) + return if record.id.to_s != join_part.parent.record_id(row).to_s - macro = join.reflection.macro + macro = join_part.reflection.macro if macro == :has_one - return if record.instance_variable_defined?("@#{join.reflection.name}") - association = join.instantiate(row) unless row[join.aliased_primary_key].nil? - set_target_and_inverse(join, association, record) + return if record.instance_variable_defined?("@#{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 - return if row[join.aliased_primary_key].nil? - association = join.instantiate(row) + return if row[join_part.aliased_primary_key].nil? + association = join_part.instantiate(row) case macro when :has_many, :has_and_belongs_to_many - collection = record.send(join.reflection.name) + collection = record.send(join_part.reflection.name) collection.loaded collection.target.push(association) collection.__send__(:set_inverse_instance, association, record) when :belongs_to - set_target_and_inverse(join, association, record) + set_target_and_inverse(join_part, association, record) else - raise ConfigurationError, "unknown macro: #{join.reflection.macro}" + raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}" end end association end - def set_target_and_inverse(join, association, record) - association_proxy = record.send("set_#{join.reflection.name}_target", association) + def set_target_and_inverse(join_part, association, record) + association_proxy = record.send("set_#{join_part.reflection.name}_target", association) association_proxy.__send__(:set_inverse_instance, association, record) end - - class JoinBase # :nodoc: - attr_reader :active_record, :table_joins - delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :arel_engine, :to => :active_record - - def initialize(active_record, joins = nil) + + # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited + # by JoinBase and JoinAssociation. A JoinBase represents the Active Record which + # everything else is being joined onto. A JoinAssociation represents an association which + # is joining to the base. A JoinAssociation may result in more than one actual join + # operations (for example a has_and_belongs_to_many JoinAssociation would result in + # two; one for the join table and one for the target table). + class JoinPart # :nodoc: + # 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. + attr_reader :active_record + + delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :arel_engine, :to => :active_record + + def initialize(active_record) @active_record = active_record @cached_record = {} - @table_joins = joins end - + def ==(other) - other.class == self.class && - other.active_record == active_record && - other.table_joins == table_joins + raise NotImplementedError 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 - "t0" + 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 - - def aliased_table_name - active_record.table_name - end - + + # An array of [column_name, alias] pairs for the table def column_names_with_alias unless defined?(@column_names_with_alias) @column_names_with_alias = [] @@ -2059,33 +2082,74 @@ module ActiveRecord end end - class JoinAssociation < JoinBase # :nodoc: - attr_reader :reflection, :parent, :aliased_table_name, :aliased_prefix, :aliased_join_table_name, :parent_table_name, :join_class - delegate :options, :klass, :through_reflection, :source_reflection, :to => :reflection + class JoinBase < JoinPart # :nodoc: + # Extra joins provided when the JoinDependency was created + attr_reader :table_joins + + def initialize(active_record, joins = nil) + super(active_record) + @table_joins = joins + end + + def ==(other) + other.class == self.class && + other.active_record == active_record && + other.table_joins == table_joins + end + + def aliased_prefix + "t0" + end + + def table + Arel::Table.new(table_name, :engine => arel_engine, :columns => active_record.columns) + end + + def aliased_table_name + active_record.table_name + end + end + + class JoinAssociation < JoinPart # :nodoc: + # The reflection of the association represented + attr_reader :reflection + + # The JoinDependency object which this JoinAssociation exists within. This is mainly + # relevant for generating aliases which do not conflict with other joins which are + # part of the query. + attr_reader :join_dependency + + # 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 + + # 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 + + delegate :options, :through_reflection, :source_reflection, :to => :reflection + delegate :table, :table_name, :to => :parent, :prefix => true def initialize(reflection, join_dependency, parent = nil) reflection.check_validity! + if reflection.options[:polymorphic] raise EagerLoadPolymorphicError.new(reflection) end super(reflection.klass) - @join_dependency = join_dependency - @parent = parent - @reflection = reflection - @aliased_prefix = "t#{ join_dependency.joins.size }" - @parent_table_name = parent.active_record.table_name - @aliased_table_name = aliased_table_name_for(table_name) - @join = nil - @join_class = Arel::InnerJoin - - if reflection.macro == :has_and_belongs_to_many - @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join") - end - - if [: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 + + @reflection = reflection + @join_dependency = join_dependency + @parent = parent + @join_type = Arel::InnerJoin + + # 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 end def ==(other) @@ -2095,63 +2159,29 @@ module ActiveRecord end def find_parent_in(other_join_dependency) - other_join_dependency.joins.detect do |join| - self.parent == join + other_join_dependency.join_parts.detect do |join_part| + self.parent == join_part end end - - def with_join_class(join_class) - @join_class = join_class - self - end - - def association_join - return @join if @join - - aliased_table = Arel::Table.new(table_name, :as => @aliased_table_name, - :engine => arel_engine, - :columns => klass.columns) - - parent_table = Arel::Table.new(parent.table_name, :as => parent.aliased_table_name, - :engine => arel_engine, - :columns => parent.active_record.columns) - - @join = send("build_#{reflection.macro}", aliased_table, parent_table) - - unless klass.descends_from_active_record? - sti_column = aliased_table[klass.inheritance_column] - sti_condition = sti_column.eq(klass.sti_name) - klass.descendants.each {|subclass| sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) } - - @join << sti_condition - end - - [through_reflection, reflection].each do |ref| - if ref && ref.options[:conditions] - @join << interpolate_sql(sanitize_sql(ref.options[:conditions], aliased_table_name)) - end - end - - @join + + def join_to(relation) + send("join_#{reflection.macro}_to", relation) end - def relation - aliased = Arel::Table.new(table_name, :as => @aliased_table_name, - :engine => arel_engine, - :columns => klass.columns) - - if reflection.macro == :has_and_belongs_to_many - [Arel::Table.new(options[:join_table], :as => aliased_join_table_name, :engine => arel_engine), aliased] - elsif reflection.options[:through] - [Arel::Table.new(through_reflection.klass.table_name, :as => aliased_join_table_name, :engine => arel_engine), aliased] - else - aliased - end + def join_relation(joining_relation) + self.join_type = Arel::OuterJoin + joining_relation.joins(self) end - - def join_relation(joining_relation, join = nil) - joining_relation.joins(self.with_join_class(Arel::OuterJoin)) + + def table + @table ||= Arel::Table.new( + table_name, :as => aliased_table_name, + :engine => arel_engine, :columns => active_record.columns + ) end + + # More semantic name given we are talking about associations + alias_method :target_table, :table protected @@ -2185,7 +2215,7 @@ module ActiveRecord end def table_name_and_alias - table_alias_for table_name, @aliased_table_name + table_alias_for table_name, aliased_table_name end def interpolate_sql(sql) @@ -2193,74 +2223,169 @@ module ActiveRecord 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] + + 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 + + conditions << sti_condition + end + + # If the reflection has conditions, add them + if options[:conditions] + conditions << process_conditions(options[:conditions], aliased_table_name) + end + + relation = relation.on(*conditions) + end - def build_has_and_belongs_to_many(aliased_table, parent_table) - join_table = Arel::Table.new(options[:join_table], :as => aliased_join_table_name, :engine => arel_engine) - fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key - klass_fk = options[:association_foreign_key] || klass.to_s.foreign_key - - [ - join_table[fk].eq(parent_table[reflection.active_record.primary_key]), - aliased_table[klass.primary_key].eq(join_table[klass_fk]) - ] + 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 build_has_many(aliased_table, parent_table) + def join_has_many_to(relation) if reflection.options[:through] - join_table = Arel::Table.new(through_reflection.klass.table_name, - :as => aliased_join_table_name, - :engine => arel_engine) - jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil - 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' - jt_as_extra = 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 + 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]) + ) + 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' + + jt_conditions << + join_table[as_key + '_type']. + eq(parent.active_record.base_class.name) + else + jt_foreign_key = through_reflection.primary_key_name + end - 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 + case source_reflection.macro + when :has_many + second_key = options[:foreign_key] || primary_key - unless through_reflection.klass.descends_from_active_record? - jt_sti_extra = join_table[through_reflection.active_record.inheritance_column].eq(through_reflection.klass.sti_name) - end - when :belongs_to - first_key = primary_key - if reflection.options[:source_type] - second_key = source_reflection.association_foreign_key - jt_source_extra = join_table[reflection.source_reflection.options[:foreign_type]].eq(reflection.options[:source_type]) - else - second_key = source_reflection.primary_key_name - end + 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 - [ - [parent_table[parent.primary_key].eq(join_table[jt_foreign_key]), jt_as_extra, jt_source_extra, jt_sti_extra].compact, - aliased_table[first_key].eq(join_table[second_key]) - ] - elsif reflection.options[:as] - id_rel = aliased_table["#{reflection.options[:as]}_id"].eq(parent_table[parent.primary_key]) - type_rel = aliased_table["#{reflection.options[:as]}_type"].eq(parent.active_record.base_class.name) - [id_rel, type_rel] - else - foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key - [aliased_table[foreign_key].eq(parent_table[reflection.options[:primary_key] || parent.primary_key])] + 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 + + 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 + end + 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 - alias :build_has_one :build_has_many - def build_belongs_to(aliased_table, parent_table) - [aliased_table[options[:primary_key] || reflection.klass.primary_key].eq(parent_table[options[:foreign_key] || reflection.primary_key_name])] + 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 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..90ebadda89 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 @@ -19,9 +21,9 @@ module ActiveRecord # Build SQL conditions from attributes, qualified by table name. 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 + conditions = construct_quoted_owner_attributes(reflection).map do |attr, value| + "#{table_aliases[reflection]}.#{attr} = #{value}" end conditions << sql_conditions if sql_conditions "(" + conditions.join(') AND (') + ")" @@ -49,35 +51,87 @@ 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]) - ] + # puts @reflection.through_reflection_chain.map(&:inspect) + + "#{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| + polymorphic_join = nil + + case + when left.source_reflection.nil? + left_primary_key = left.primary_key_name + right_primary_key = right.klass.primary_key + + if left.options[:as] + polymorphic_join = "AND %s.%s = %s" % [ + table_aliases[left], "#{left.options[:as]}_type", + @owner.class.quote_value(right.klass.name) + ] + end + when left.source_reflection.macro == :belongs_to + left_primary_key = left.klass.primary_key + right_primary_key = left.source_reflection.primary_key_name + + if left.options[:source_type] + polymorphic_join = "AND %s.%s = %s" % [ + table_aliases[right], + left.source_reflection.options[:foreign_type].to_s, + @owner.class.quote_value(left.options[:source_type]) + ] + end + else + left_primary_key = left.source_reflection.primary_key_name + right_primary_key = right.klass.primary_key + + if left.source_reflection.options[:as] + polymorphic_join = "AND %s.%s = %s" % [ + table_aliases[left], + "#{left.source_reflection.options[:as]}_type", + @owner.class.quote_value(right.klass.name) + ] + end 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) - ] + + if right.quoted_table_name == table_aliases[right] + table = right.quoted_table_name + else + table = "#{right.quoted_table_name} #{table_aliases[right]}" end + + joins << "INNER JOIN %s ON %s.%s = %s.%s %s" % [ + table, + table_aliases[left], left_primary_key, + table_aliases[right], right_primary_key, + polymorphic_join + ] end + + joins.join(" ") + 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 + tally = {} + @reflection.through_reflection_chain.inject({}) do |aliases, reflection| + if tally[reflection.table_name].nil? + tally[reflection.table_name] = 1 + aliases[reflection] = reflection.quoted_table_name + else + tally[reflection.table_name] += 1 + aliased_table_name = reflection.table_name + "_#{tally[reflection.table_name]}" + aliases[reflection] = reflection.klass.connection.quote_table_name(aliased_table_name) + end + aliases + end + end end # Construct attributes for associate pointing to owner. 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 diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index ede1c8821e..5034caf084 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -343,8 +343,11 @@ module ActiveRecord end def column_aliases(join_dependency) - join_dependency.joins.collect{|join| join.column_names_with_alias.collect{|column_name, aliased_name| - "#{connection.quote_table_name join.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}"}}.flatten.join(", ") + join_dependency.join_parts.collect { |join_part| + join_part.column_names_with_alias.collect{ |column_name, aliased_name| + "#{connection.quote_table_name join_part.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}" + } + }.flatten.join(", ") end def using_limitable_reflections?(reflections) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 001207514d..f314ff861f 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -230,19 +230,8 @@ module ActiveRecord @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.uniq.each do |left, join_class, right| - relation = relation.join(left, join_class).on(*right) + relation = association.join_to(relation) end relation.join(custom_joins) |