diff options
Diffstat (limited to 'activerecord/lib/active_record/associations/through_association.rb')
-rw-r--r-- | activerecord/lib/active_record/associations/through_association.rb | 221 |
1 files changed, 172 insertions, 49 deletions
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index e1d60ccb17..ed24373cba 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -1,9 +1,12 @@ +require 'enumerator' + module ActiveRecord # = Active Record Through Association module Associations module ThroughAssociation #:nodoc: - delegate :source_options, :through_options, :source_reflection, :through_reflection, :to => :reflection + delegate :source_options, :through_options, :source_reflection, :through_reflection, + :through_reflection_chain, :through_conditions, :to => :reflection protected @@ -13,10 +16,12 @@ module ActiveRecord def association_scope scope = super.joins(construct_joins) - scope = add_conditions(scope) + scope = scope.where(reflection_conditions(0)) + unless options[:include] scope = scope.includes(source_options[:include]) end + scope end @@ -30,6 +35,7 @@ module ActiveRecord { } end + # TODO: Needed? def aliased_through_table name = through_reflection.table_name @@ -39,41 +45,101 @@ module ActiveRecord end def construct_owner_conditions - super(aliased_through_table, through_reflection) + reflection = through_reflection_chain.last + + if reflection.macro == :has_and_belongs_to_many + table = tables[reflection].first + else + table = Array.wrap(tables[reflection]).first + end + + super(table, reflection) end def construct_joins - right = aliased_through_table - left = reflection.klass.arel_table + joins, right_index = [], 1 - conditions = [] + # Iterate over each pair in the through reflection chain, joining them together + through_reflection_chain.each_cons(2) do |left, right| + left_table, right_table = tables[left], tables[right] - if source_reflection.macro == :belongs_to - reflection_primary_key = source_reflection.association_primary_key - source_primary_key = source_reflection.foreign_key + if left.source_reflection.nil? + case left.macro + when :belongs_to + joins << inner_join( + right_table, + left_table[left.association_primary_key], + right_table[left.foreign_key], + reflection_conditions(right_index) + ) + when :has_many, :has_one + joins << inner_join( + right_table, + left_table[left.foreign_key], + right_table[right.association_primary_key], + polymorphic_conditions(left, left), + reflection_conditions(right_index) + ) + when :has_and_belongs_to_many + joins << inner_join( + right_table, + left_table.first[left.foreign_key], + right_table[right.klass.primary_key], + reflection_conditions(right_index) + ) + end + else + case left.source_reflection.macro + when :belongs_to + joins << inner_join( + right_table, + left_table[left.association_primary_key], + right_table[left.foreign_key], + 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 = tables[right] + end - if options[:source_type] - column = source_reflection.foreign_type - conditions << - right[column].eq(options[:source_type]) - end - else - reflection_primary_key = source_reflection.foreign_key - source_primary_key = source_reflection.active_record_primary_key + joins << inner_join( + right_table, + left_table[left.foreign_key], + right_table[left.source_reflection.active_record_primary_key], + polymorphic_conditions(left, left.source_reflection), + reflection_conditions(right_index) + ) - if source_options[:as] - column = "#{source_options[:as]}_type" - conditions << - left[column].eq(through_reflection.klass.name) + if right.macro == :has_and_belongs_to_many + joins << inner_join( + 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 = tables[left] + + joins << inner_join( + join_table, + left_table[left.klass.primary_key], + join_table[left.association_foreign_key] + ) + + joins << inner_join( + right_table, + join_table[left.foreign_key], + right_table[right.klass.primary_key], + reflection_conditions(right_index) + ) + end end - end - conditions << - left[reflection_primary_key].eq(right[source_primary_key]) + right_index += 1 + end - right.create_join( - right, - right.create_on(right.create_and(conditions))) + joins end # Construct attributes for :through pointing to owner and associate. This is used by the @@ -112,37 +178,88 @@ module ActiveRecord end end - # The reason that we are operating directly on the scope here (rather than passing - # back some arel conditions to be added to the scope) is because scope.where([x, y]) - # has a different meaning to scope.where(x).where(y) - the first version might - # perform some substitution if x is a string. - def add_conditions(scope) - unless through_reflection.klass.descends_from_active_record? - scope = scope.where(through_reflection.klass.send(:type_condition)) + def alias_tracker + @alias_tracker ||= AliasTracker.new + end + + # TODO: It is decidedly icky to have an array for habtm entries, and no array for others + def tables + @tables ||= begin + Hash[ + through_reflection_chain.map do |reflection| + table = alias_tracker.aliased_table_for( + reflection.table_name, + table_alias_for(reflection, reflection != self.reflection) + ) + + if reflection.macro == :has_and_belongs_to_many || + (reflection.source_reflection && + reflection.source_reflection.macro == :has_and_belongs_to_many) + + join_table = alias_tracker.aliased_table_for( + (reflection.source_reflection || reflection).options[:join_table], + table_alias_for(reflection, true) + ) + + [reflection, [join_table, table]] + else + [reflection, table] + end + end + ] end + end - scope = scope.where(interpolate(source_options[:conditions])) - scope.where(through_conditions) + def table_alias_for(reflection, join = false) + name = alias_tracker.pluralize(reflection.name) + name << "_#{self.reflection.name}" + name << "_join" if join + name end - # If there is a hash of conditions then we make sure the keys are scoped to the - # through table name if left ambiguous. - def through_conditions - conditions = interpolate(through_options[:conditions]) + def inner_join(table, left_column, right_column, *conditions) + conditions << left_column.eq(right_column) - if conditions.is_a?(Hash) - Hash[conditions.map { |key, value| - unless value.is_a?(Hash) || key.to_s.include?('.') - key = aliased_through_table.name + '.' + key.to_s - end + table.create_join( + table, + table.create_on(table.create_and(conditions.flatten.compact))) + end - [key, value] - }] - else - conditions + def reflection_conditions(index) + reflection = through_reflection_chain[index] + conditions = through_conditions[index].dup + + # TODO: maybe this should go in Reflection#through_conditions directly? + unless reflection.klass.descends_from_active_record? + conditions << reflection.klass.send(:type_condition) + end + + unless conditions.empty? + conditions.map! do |condition| + condition = reflection.klass.send(:sanitize_sql, interpolate(condition), reflection.table_name) + condition = Arel.sql(condition) unless condition.is_a?(Arel::Node) + condition + end + + Arel::Nodes::And.new(conditions) end end + def polymorphic_conditions(reflection, polymorphic_reflection) + if polymorphic_reflection.options[:as] + tables[reflection][polymorphic_reflection.type]. + eq(polymorphic_reflection.active_record.base_class.name) + end + end + + def source_type_conditions(reflection) + if reflection.options[:source_type] + tables[reflection.through_reflection][reflection.foreign_type]. + eq(reflection.options[:source_type]) + end + end + + # TODO: Think about this in the context of nested associations def stale_state if through_reflection.macro == :belongs_to owner[through_reflection.foreign_key].to_s @@ -153,6 +270,12 @@ module ActiveRecord through_reflection.macro == :belongs_to && !owner[through_reflection.foreign_key].nil? end + + def ensure_not_nested + if reflection.nested? + raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) + end + end end end end |