aboutsummaryrefslogblamecommitdiffstats
path: root/activerecord/lib/active_record/associations/through_association.rb
blob: ed24373cbaa21a94ec48c7821ba577b715e115b1 (plain) (tree)
1
2
3
4
5
6
7
8
9

                    
                   
                                       
                     
                                      
 

                                                                                          
 

               
                        
                                                      
           
 
                             
                                              

                                                       

                                                            
             
 
               
           
 

             






                                                                                                   
 
                       
                                 
                                              
 
                                         

                                                                       
           
 
                                      








                                                         
           
 
                           
                                    
 


                                                                                         
 






































                                                               
 






                                                                                  
 






















                                                               
               
 

                            
 
               

           












                                                                                                  
                                                   
                                                                                                 
             
 
                             
                                            
                                    
                                                                      
               
           
 
                                  
                                                             
                                                                   
             
 




                                                             

           




























                                                                                                
             
           
 




                                                         
           
 

                                                                     
 



                                                                          
 
















                                                                                                             
             
           
 














                                                                           
                       

                                                      
             
           
 
                                

                                                     
           





                                                                                    


       
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