diff options
Diffstat (limited to 'activerecord/lib/active_record/associations/through_association_scope.rb')
-rw-r--r-- | activerecord/lib/active_record/associations/through_association_scope.rb | 290 |
1 files changed, 216 insertions, 74 deletions
diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index acddfda924..f6d02a215f 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,70 +20,244 @@ module ActiveRecord end def construct_create_scope - construct_owner_attributes(@reflection) + @reflection.nested? ? {} : construct_owner_attributes(@reflection) end # 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 + + if reflection.macro == :has_and_belongs_to_many + table_alias = table_aliases[reflection].first + else + table_alias = table_aliases[reflection] + end + + parts = construct_quoted_owner_attributes(reflection).map do |attr, value| + "#{table_alias}.#{attr} = #{value}" end - conditions << sql_conditions if sql_conditions - "(" + conditions.join(') AND (') + ")" + parts += reflection_conditions(0) + + "(" + parts.join(') AND (') + ")" end # Associate attributes pointing to owner, quoted. def construct_quoted_owner_attributes(reflection) if as = reflection.options[:as] - { "#{as}_id" => owner_quoted_id, + { "#{as}_id" => owner_quoted_id(reflection), "#{as}_type" => reflection.klass.quote_value( @owner.class.base_class.name.to_s, reflection.klass.columns_hash["#{as}_type"]) } elsif reflection.macro == :belongs_to { reflection.klass.primary_key => @owner.class.quote_value(@owner[reflection.primary_key_name]) } else - { reflection.primary_key_name => owner_quoted_id } + { reflection.primary_key_name => owner_quoted_id(reflection) } end end - def construct_from - @reflection.table_name - end - def construct_select(custom_select = nil) distinct = "DISTINCT " if @reflection.options[:uniq] 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]) - ] + "#{construct_through_joins} #{@reflection.options[:joins]} #{custom_joins}" + end + + def construct_through_joins + joins, right_index = [], 1 + + # 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? + case left.macro + when :belongs_to + joins << inner_join_sql( + right_table_and_alias, + table_aliases[left], left.association_primary_key, + table_aliases[right], left.primary_key_name, + reflection_conditions(right_index) + ) + when :has_many, :has_one + joins << inner_join_sql( + right_table_and_alias, + table_aliases[left], left.primary_key_name, + table_aliases[right], right.association_primary_key, + polymorphic_conditions(left, left), + reflection_conditions(right_index) + ) + when :has_and_belongs_to_many + joins << inner_join_sql( + right_table_and_alias, + table_aliases[left].first, left.primary_key_name, + table_aliases[right], right.klass.primary_key, + reflection_conditions(right_index) + ) + end + else + case left.source_reflection.macro + when :belongs_to + joins << inner_join_sql( + right_table_and_alias, + table_aliases[left], left.association_primary_key, + table_aliases[right], left.primary_key_name, + source_type_conditions(left), + reflection_conditions(right_index) + ) + 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.primary_key_name, + right_table, left.source_reflection.active_record_primary_key, + polymorphic_conditions(left, left.source_reflection), + reflection_conditions(right_index) + ) + + 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.association_foreign_key + ) + + joins << inner_join_sql( + right_table_and_alias, + join_table, left.primary_key_name, + table_aliases[right], right.klass.primary_key, + reflection_conditions(right_index) + ) + 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) - ] + + right_index += 1 + end + + joins.join(" ") + end + + def alias_tracker + @alias_tracker ||= AliasTracker.new + end + + 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, *conditions) + conditions << "#{on_left_table}.#{on_left_key} = #{on_right_table}.#{on_right_key}" + conditions = conditions.flatten.compact + conditions = conditions.map { |sql| "(#{sql})" } * ' AND ' + + "INNER JOIN #{table} ON #{conditions}" + end + + def reflection_conditions(index) + reflection = @reflection.through_reflection_chain[index] + reflection_conditions = @reflection.through_conditions[index] + + conditions = [] + + if reflection.options[:as].nil? && # reflection.klass is a Module if :as is used + reflection.klass.finder_needs_type_condition? + conditions << reflection.klass.send(:type_condition).to_sql + end + + reflection_conditions.each do |condition| + sanitized_condition = reflection.klass.send(:sanitize_sql, condition) + interpolated_condition = interpolate_sql(sanitized_condition) + + if condition.is_a?(Hash) + interpolated_condition.gsub!( + @reflection.quoted_table_name, + reflection.quoted_table_name + ) end + + conditions << interpolated_condition 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 - ] + conditions + end + + def polymorphic_conditions(reflection, polymorphic_reflection) + if polymorphic_reflection.options[:as] + "%s.%s = %s" % [ + table_aliases[reflection], "#{polymorphic_reflection.options[:as]}_type", + @owner.class.quote_value(polymorphic_reflection.active_record.base_class.name) + ] + end + end + + def source_type_conditions(reflection) + if reflection.options[:source_type] + "%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. @@ -95,6 +271,8 @@ module ActiveRecord end # Construct attributes for :through pointing to owner and associate. + # This method is used when adding records to the association. Since this only makes sense for + # non-nested through associations, that's the only case we have to worry about here. def construct_join_attributes(associate) # TODO: revisit this to allow it for deletion, supposing dependent option is supported raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro) @@ -112,47 +290,11 @@ module ActiveRecord join_attributes end - def conditions - @conditions = build_conditions unless defined?(@conditions) - @conditions - end - - def build_conditions - association_conditions = @reflection.options[:conditions] - through_conditions = build_through_conditions - source_conditions = @reflection.source_reflection.options[:conditions] - uses_sti = !@reflection.through_reflection.klass.descends_from_active_record? - - if association_conditions || through_conditions || source_conditions || uses_sti - all = [] - - [association_conditions, source_conditions].each do |conditions| - all << interpolate_sql(sanitize_sql(conditions)) if conditions - end - - all << through_conditions if through_conditions - all << build_sti_condition if uses_sti - - all.map { |sql| "(#{sql})" } * ' AND ' - end - end - - def build_through_conditions - conditions = @reflection.through_reflection.options[:conditions] - if conditions.is_a?(Hash) - interpolate_sql(@reflection.through_reflection.klass.send(:sanitize_sql, conditions)).gsub( - @reflection.quoted_table_name, - @reflection.through_reflection.quoted_table_name) - elsif conditions - interpolate_sql(sanitize_sql(conditions)) + def ensure_not_nested + if @reflection.nested? + raise HasManyThroughNestedAssociationsAreReadonly.new(@owner, @reflection) end end - - def build_sti_condition - @reflection.through_reflection.klass.send(:type_condition).to_sql - end - - alias_method :sql_conditions, :conditions end end end |