require 'enumerator' module ActiveRecord # = Active Record Through Association module Associations module ThroughAssociation #:nodoc: delegate :source_options, :through_options, :source_reflection, :through_reflection, :through_reflection_chain, :through_conditions, :to => :reflection protected def target_scope super.merge(through_reflection.klass.scoped) end def association_scope scope = super.joins(construct_joins) scope = scope.where(reflection_conditions(0)) unless options[:include] scope = scope.includes(source_options[:include]) end scope end private # This scope affects the creation of the associated records (not the join records). At the # moment we only support creating on a :through association when the source reflection is a # belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so # this scope has can legitimately be empty. def creation_attributes { } end # TODO: Needed? def aliased_through_table name = through_reflection.table_name reflection.table_name == name ? through_reflection.klass.arel_table.alias(name + "_join") : through_reflection.klass.arel_table end def construct_owner_conditions 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 joins, right_index = [], 1 # 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 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 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 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 right_index += 1 end joins end # Construct attributes for :through pointing to owner and associate. This is used by the # methods which create and delete records on the association. # # We only support indirectly modifying through associations which has a belongs_to source. # This is the "has_many :tags, :through => :taggings" situation, where the join model # typically has a belongs_to on both side. In other words, associations which could also # be represented as has_and_belongs_to_many associations. # # We do not support creating/deleting records on the association where the source has # some other type, because this opens up a whole can of worms, and in basically any # situation it is more natural for the user to just create or modify their join records # directly as required. def construct_join_attributes(*records) if source_reflection.macro != :belongs_to raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) end join_attributes = { source_reflection.foreign_key => records.map { |record| record.send(source_reflection.association_primary_key) } } if options[:source_type] join_attributes[source_reflection.foreign_type] = records.map { |record| record.class.base_class.name } end if records.count == 1 Hash[join_attributes.map { |k, v| [k, v.first] }] else join_attributes end end 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 def table_alias_for(reflection, join = false) name = alias_tracker.pluralize(reflection.name) name << "_#{self.reflection.name}" name << "_join" if join name end def inner_join(table, left_column, right_column, *conditions) conditions << left_column.eq(right_column) table.create_join( table, table.create_on(table.create_and(conditions.flatten.compact))) end 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 end end def foreign_key_present? 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