aboutsummaryrefslogblamecommitdiffstats
path: root/activerecord/lib/active_record/associations/join_dependency/join_association.rb
blob: 890e77fca9cac1dc90b95bc5e0702cf51866f801 (plain) (tree)



















                                                                                                  
                                   
 


                                                                                                                 
                                                                        
                                                        















                                                                    
                      














                                                                
























































































                                                                                               






                                           


                                     
              
                       
             
           
 

                                         

           






                                                         



               


















                                                                                                                 
 



                                 
             




                                                                                               













                                                                  




                                                                          
 




                                                                      
 

                                                                     


                                                       

           



                                                                     

             
 



                                                          
             

           



         
module ActiveRecord
  module Associations
    class JoinDependency # :nodoc:
      class JoinAssociation < JoinPart # :nodoc:
        # The reflection of the association represented
        attr_reader :reflection

        # The JoinDependency object which this JoinAssociation exists within. This is mainly
        # relevant for generating aliases which do not conflict with other joins which are
        # part of the query.
        attr_reader :join_dependency

        # A JoinBase instance representing the active record we are joining onto.
        # (So in Author.has_many :posts, the Author would be that base record.)
        attr_reader :parent

        # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
        attr_accessor :join_type

        # These implement abstract methods from the superclass
        attr_reader :aliased_prefix

        attr_reader :tables

        delegate :options, :through_reflection, :source_reflection, :through_reflection_chain, :to => :reflection
        delegate :table, :table_name, :to => :parent, :prefix => :parent
        delegate :alias_tracker, :to => :join_dependency

        def initialize(reflection, join_dependency, parent = nil)
          reflection.check_validity!

          if reflection.options[:polymorphic]
            raise EagerLoadPolymorphicError.new(reflection)
          end

          super(reflection.klass)

          @reflection      = reflection
          @join_dependency = join_dependency
          @parent          = parent
          @join_type       = Arel::InnerJoin
          @aliased_prefix  = "t#{ join_dependency.join_parts.size }"

          setup_tables
        end

        def ==(other)
          other.class == self.class &&
            other.reflection == reflection &&
            other.parent == parent
        end

        def find_parent_in(other_join_dependency)
          other_join_dependency.join_parts.detect do |join_part|
            parent == join_part
          end
        end

        def join_to(relation)
          # The chain starts with the target table, but we want to end with it here (makes
          # more sense in this context)
          chain = through_reflection_chain.reverse

          foreign_table = parent_table
          index = 0

          chain.each do |reflection|
            table = tables[index]
            conditions = []

            if reflection.source_reflection.nil?
              case reflection.macro
                when :belongs_to
                  key         = reflection.association_primary_key
                  foreign_key = reflection.foreign_key
                when :has_many, :has_one
                  key         = reflection.foreign_key
                  foreign_key = reflection.active_record_primary_key

                  conditions << polymorphic_conditions(reflection, table)
                when :has_and_belongs_to_many
                  # For habtm, we need to deal with the join table at the same time as the
                  # target table (because unlike a :through association, there is no reflection
                  # to represent the join table)
                  table, join_table = table

                  join_key         = reflection.foreign_key
                  join_foreign_key = reflection.active_record.primary_key

                  relation = relation.join(join_table, join_type).on(
                    join_table[join_key].
                      eq(foreign_table[join_foreign_key])
                  )

                  # We've done the first join now, so update the foreign_table for the second
                  foreign_table = join_table

                  key         = reflection.klass.primary_key
                  foreign_key = reflection.association_foreign_key
              end
            else
              case reflection.source_reflection.macro
                when :belongs_to
                  key         = reflection.association_primary_key
                  foreign_key = reflection.foreign_key

                  conditions << source_type_conditions(reflection, foreign_table)
                when :has_many, :has_one
                  key         = reflection.foreign_key
                  foreign_key = reflection.source_reflection.active_record_primary_key
                when :has_and_belongs_to_many
                  table, join_table = table

                  join_key         = reflection.foreign_key
                  join_foreign_key = reflection.klass.primary_key

                  relation = relation.join(join_table, join_type).on(
                    join_table[join_key].
                      eq(foreign_table[join_foreign_key])
                  )

                  foreign_table = join_table

                  key         = reflection.klass.primary_key
                  foreign_key = reflection.association_foreign_key
              end
            end

            conditions << table[key].eq(foreign_table[foreign_key])

            conditions << reflection_conditions(index, table)
            conditions << sti_conditions(reflection, table)

            ands = relation.create_and(conditions.flatten.compact)

            join = relation.create_join(
              table,
              relation.create_on(ands),
              join_type)

            relation = relation.from(join)

            # The current table in this iteration becomes the foreign table in the next
            foreign_table = table
            index += 1
          end

          relation
        end

        def join_relation(joining_relation)
          self.join_type = Arel::OuterJoin
          joining_relation.joins(self)
        end

        def table
          if tables.last.is_a?(Array)
            tables.last.first
          else
            tables.last
          end
        end

        def aliased_table_name
          table.table_alias || table.name
        end

        protected

        def table_alias_for(reflection, join = false)
          name = alias_tracker.pluralize(reflection.name)
          name << "_#{parent_table_name}"
          name << "_join" if join
          name
        end

        private

        # Generate aliases and Arel::Table instances for each of the tables which we will
        # later generate joins for. We must do this in advance in order to correctly allocate
        # the proper alias.
        def setup_tables
          @tables = through_reflection_chain.map do |reflection|
            table = alias_tracker.aliased_table_for(
              reflection.table_name,
              table_alias_for(reflection, reflection != self.reflection)
            )

            # For habtm, we have two Arel::Table instances related to a single reflection, so
            # we just store them as a pair in the array.
            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)
              )

              [table, join_table]
            else
              table
            end
          end

          # The joins are generated from the through_reflection_chain in reverse order, so
          # reverse the tables too (but it's important to generate the aliases in the 'forward'
          # order, which is why we only do the reversal now.
          @tables.reverse!
        end

        def process_conditions(conditions, table_name)
          if conditions.respond_to?(:to_proc)
            conditions = instance_eval(&conditions)
          end

          Arel.sql(sanitize_sql(conditions, table_name))
        end

        def sanitize_sql(condition, table_name)
          active_record.send(:sanitize_sql, condition, table_name)
        end

        def reflection_conditions(index, table)
          reflection.through_conditions.reverse[index].map do |condition|
            process_conditions(condition, table.table_alias || table.name)
          end
        end

        def sti_conditions(reflection, table)
          unless reflection.klass.descends_from_active_record?
            sti_column    = table[reflection.klass.inheritance_column]
            sti_condition = sti_column.eq(reflection.klass.sti_name)
            subclasses    = reflection.klass.descendants

            # TODO: use IN (...), or possibly AR::Base#type_condition
            subclasses.inject(sti_condition) { |attr,subclass|
              attr.or(sti_column.eq(subclass.sti_name))
            }
          end
        end

        def source_type_conditions(reflection, foreign_table)
          if reflection.options[:source_type]
            foreign_table[reflection.source_reflection.foreign_type].
              eq(reflection.options[:source_type])
          end
        end

        def polymorphic_conditions(reflection, table)
          if reflection.options[:as]
            table[reflection.type].
              eq(reflection.active_record.base_class.name)
          end
        end

      end
    end
  end
end