module ActiveRecord module Associations module ClassMethods 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, :aliased_table_name delegate :options, :through_reflection, :source_reflection, :to => :reflection delegate :table, :table_name, :to => :parent, :prefix => true 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 # This must be done eagerly upon initialisation because the alias which is produced # depends on the state of the join dependency, but we want it to work the same way # every time. allocate_aliases 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| self.parent == join_part end end def join_to(relation) send("join_#{reflection.macro}_to", relation) end def join_relation(joining_relation) self.join_type = Arel::OuterJoin joining_relation.joins(self) end def table @table ||= Arel::Table.new( table_name, :as => aliased_table_name, :engine => arel_engine, :columns => active_record.columns ) end # More semantic name given we are talking about associations alias_method :target_table, :table protected def aliased_table_name_for(name, suffix = nil) if @join_dependency.table_aliases[name].zero? @join_dependency.table_aliases[name] = @join_dependency.count_aliases_from_table_joins(name) end if !@join_dependency.table_aliases[name].zero? # We need an alias name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}" @join_dependency.table_aliases[name] += 1 if @join_dependency.table_aliases[name] == 1 # First time we've seen this name # Also need to count the aliases from the table_aliases to avoid incorrect count @join_dependency.table_aliases[name] += @join_dependency.count_aliases_from_table_joins(name) end table_index = @join_dependency.table_aliases[name] name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index}" if table_index > 1 else @join_dependency.table_aliases[name] += 1 end name end def pluralize(table_name) ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name end def interpolate_sql(sql) instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__) end private def allocate_aliases @aliased_prefix = "t#{ join_dependency.join_parts.size }" @aliased_table_name = aliased_table_name_for(table_name) if reflection.macro == :has_and_belongs_to_many @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join") elsif [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through] @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join") end end def process_conditions(conditions, table_name) Arel.sql(interpolate_sql(sanitize_sql(conditions, table_name))) end def join_target_table(relation, *conditions) relation = relation.join(target_table, join_type) # If the target table is an STI model then we must be sure to only include records of # its type and its sub-types. unless active_record.descends_from_active_record? sti_column = target_table[active_record.inheritance_column] sti_condition = sti_column.eq(active_record.sti_name) active_record.descendants.each do |subclass| sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) end conditions << sti_condition end # If the reflection has conditions, add them if options[:conditions] conditions << process_conditions(options[:conditions], aliased_table_name) end relation = relation.on(*conditions) end def join_has_and_belongs_to_many_to(relation) join_table = Arel::Table.new( options[:join_table], :engine => arel_engine, :as => @aliased_join_table_name ) fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key klass_fk = options[:association_foreign_key] || reflection.klass.to_s.foreign_key relation = relation.join(join_table, join_type) relation = relation.on( join_table[fk]. eq(parent_table[reflection.active_record.primary_key]) ) join_target_table( relation, target_table[reflection.klass.primary_key]. eq(join_table[klass_fk]) ) end def join_has_many_to(relation) if reflection.options[:through] join_has_many_through_to(relation) elsif reflection.options[:as] join_has_many_polymorphic_to(relation) else foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key primary_key = options[:primary_key] || parent.primary_key join_target_table( relation, target_table[foreign_key]. eq(parent_table[primary_key]) ) end end alias :join_has_one_to :join_has_many_to def join_has_many_through_to(relation) join_table = Arel::Table.new( through_reflection.klass.table_name, :engine => arel_engine, :as => @aliased_join_table_name ) jt_conditions = [] jt_foreign_key = first_key = second_key = nil if through_reflection.options[:as] # has_many :through against a polymorphic join as_key = through_reflection.options[:as].to_s jt_foreign_key = as_key + '_id' jt_conditions << join_table[as_key + '_type']. eq(parent.active_record.base_class.name) else jt_foreign_key = through_reflection.primary_key_name end case source_reflection.macro when :has_many second_key = options[:foreign_key] || primary_key if source_reflection.options[:as] first_key = "#{source_reflection.options[:as]}_id" else first_key = through_reflection.klass.base_class.to_s.foreign_key end unless through_reflection.klass.descends_from_active_record? jt_conditions << join_table[through_reflection.active_record.inheritance_column]. eq(through_reflection.klass.sti_name) end when :belongs_to first_key = primary_key if reflection.options[:source_type] second_key = source_reflection.association_foreign_key jt_conditions << join_table[reflection.source_reflection.options[:foreign_type]]. eq(reflection.options[:source_type]) else second_key = source_reflection.primary_key_name end end jt_conditions << parent_table[parent.primary_key]. eq(join_table[jt_foreign_key]) if through_reflection.options[:conditions] jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name) end relation = relation.join(join_table, join_type).on(*jt_conditions) join_target_table( relation, target_table[first_key].eq(join_table[second_key]) ) end def join_has_many_polymorphic_to(relation) join_target_table( relation, target_table["#{reflection.options[:as]}_id"]. eq(parent_table[parent.primary_key]), target_table["#{reflection.options[:as]}_type"]. eq(parent.active_record.base_class.name) ) end def join_belongs_to_to(relation) foreign_key = options[:foreign_key] || reflection.primary_key_name primary_key = options[:primary_key] || reflection.klass.primary_key join_target_table( relation, target_table[primary_key].eq(parent_table[foreign_key]) ) end end end end end end