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


               


                                                             
 
                             

                                              



                                                                                   
           
 

             






                                                                                                   
 

                                                          
 



                                                                                   
 


                                                                      
 
























                                                                                           
 

                                                                      
 


                                                          

           



                                                                                                                                                                                      
 



                                                                                   
 


                                                                                                                 
 


                                                                                       
 
                         

           






                                                                                            
             


                                                                                 
           
 


                                                                                       
                                                                          
 
                                   



                                                                 
 




                          
           
 



                                                                   
           
 



                                                                  


       
module ActiveRecord
  # = Active Record Through Association
  module Associations
    module ThroughAssociation

      protected

        def target_scope
          super & @reflection.through_reflection.klass.scoped
        end

        def association_scope
          scope = super.joins(construct_joins)
          scope = add_conditions(scope)
          unless @reflection.options[:include]
            scope = scope.includes(@reflection.source_reflection.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

        def aliased_through_table
          name = @reflection.through_reflection.table_name

          @reflection.table_name == name ?
            @reflection.through_reflection.klass.arel_table.alias(name + "_join") :
            @reflection.through_reflection.klass.arel_table
        end

        def construct_owner_conditions
          super(aliased_through_table, @reflection.through_reflection)
        end

        def construct_joins
          right = aliased_through_table
          left  = @reflection.klass.arel_table

          conditions = []

          if @reflection.source_reflection.macro == :belongs_to
            reflection_primary_key = @reflection.source_reflection.options[:primary_key] ||
                                     @reflection.klass.primary_key
            source_primary_key     = @reflection.source_reflection.foreign_key
            if @reflection.options[:source_type]
              column = @reflection.source_reflection.foreign_type
              conditions <<
                right[column].eq(@reflection.options[:source_type])
            end
          else
            reflection_primary_key = @reflection.source_reflection.foreign_key
            source_primary_key     = @reflection.source_reflection.options[:primary_key] ||
                                     @reflection.through_reflection.klass.primary_key
            if @reflection.source_reflection.options[:as]
              column = "#{@reflection.source_reflection.options[:as]}_type"
              conditions <<
                left[column].eq(@reflection.through_reflection.klass.name)
            end
          end

          conditions <<
            left[reflection_primary_key].eq(right[source_primary_key])

          right.create_join(
            right,
            right.create_on(right.create_and(conditions)))
        end

        # Construct attributes for :through pointing to owner and associate.
        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)

          join_attributes = {
            @reflection.source_reflection.foreign_key =>
              associate.send(@reflection.source_reflection.association_primary_key)
          }

          if @reflection.options[:source_type]
            join_attributes.merge!(@reflection.source_reflection.foreign_type => associate.class.base_class.name)
          end

          if @reflection.through_reflection.options[:conditions].is_a?(Hash)
            join_attributes.merge!(@reflection.through_reflection.options[:conditions])
          end

          join_attributes
        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 @reflection.through_reflection.klass.descends_from_active_record?
            scope = scope.where(@reflection.through_reflection.klass.send(:type_condition))
          end

          scope = scope.where(@reflection.source_reflection.options[:conditions])
          scope.where(through_conditions)
        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 = @reflection.through_reflection.options[:conditions]

          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

              [key, value]
            }]
          else
            conditions
          end
        end

        def stale_state
          if @reflection.through_reflection.macro == :belongs_to
            @owner[@reflection.through_reflection.foreign_key].to_s
          end
        end

        def foreign_key_present?
          @reflection.through_reflection.macro == :belongs_to &&
          !@owner[@reflection.through_reflection.foreign_key].nil?
        end
    end
  end
end