aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/through_association.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/associations/through_association.rb')
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb221
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