diff options
Diffstat (limited to 'activerecord/lib/active_record/associations/join_dependency/join_association.rb')
-rw-r--r-- | activerecord/lib/active_record/associations/join_dependency/join_association.rb | 334 |
1 files changed, 157 insertions, 177 deletions
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index ebe39c35fe..890e77fca9 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -18,10 +18,13 @@ module ActiveRecord attr_accessor :join_type # These implement abstract methods from the superclass - attr_reader :aliased_prefix, :aliased_table_name + attr_reader :aliased_prefix - delegate :options, :through_reflection, :source_reflection, :to => :reflection + 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! @@ -38,13 +41,7 @@ module ActiveRecord @join_type = Arel::InnerJoin @aliased_prefix = "t#{ join_dependency.join_parts.size }" - # 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 - @table = Arel::Table.new( - table_name, :as => aliased_table_name, :engine => arel_engine - ) + setup_tables end def ==(other) @@ -60,7 +57,95 @@ module ActiveRecord end def join_to(relation) - send("join_#{reflection.macro}_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) @@ -68,42 +153,59 @@ module ActiveRecord joining_relation.joins(self) end - attr_reader :table - # More semantic name given we are talking about associations - alias_method :target_table, :table - - protected - - def aliased_table_name_for(name, suffix = nil) - aliases = @join_dependency.table_aliases - - if aliases[name] != 0 # We need an alias - connection = active_record.connection - - name = connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}" - aliases[name] += 1 - name = name[0, connection.table_alias_length-3] + "_#{aliases[name]}" if aliases[name] > 1 + def table + if tables.last.is_a?(Array) + tables.last.first else - aliases[name] += 1 + tables.last end + end - name + def aliased_table_name + table.table_alias || table.name end - def pluralize(table_name) - ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name + 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 - def allocate_aliases - @aliased_table_name = aliased_table_name_for(table_name) + # 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) + ) - 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") + [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) @@ -118,161 +220,39 @@ module ActiveRecord active_record.send(:sanitize_sql, condition, table_name) end - def join_target_table(relation, condition) - conditions = [condition] + def reflection_conditions(index, table) + reflection.through_conditions.reverse[index].map do |condition| + process_conditions(condition, table.table_alias || table.name) + end + end - # 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] - subclasses = active_record.descendants - sti_condition = sti_column.eq(active_record.sti_name) + 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 - conditions << subclasses.inject(sti_condition) { |attr,subclass| + # TODO: use IN (...), or possibly AR::Base#type_condition + subclasses.inject(sti_condition) { |attr,subclass| attr.or(sti_column.eq(subclass.sti_name)) } end - - # If the reflection has conditions, add them - if options[:conditions] - conditions << process_conditions(options[:conditions], aliased_table_name) - end - - ands = relation.create_and(conditions) - - join = relation.create_join( - target_table, - relation.create_on(ands), - join_type) - - relation.from join - end - - def join_has_and_belongs_to_many_to(relation) - join_table = Arel::Table.new( - options[:join_table] - ).alias(@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]) - ) + 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 - 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 - ).alias @aliased_join_table_name - - jt_conditions = [] - first_key = second_key = nil - - if through_reflection.macro == :belongs_to - jt_primary_key = through_reflection.foreign_key - jt_foreign_key = through_reflection.association_primary_key - else - jt_primary_key = through_reflection.active_record_primary_key - jt_foreign_key = through_reflection.foreign_key - - if through_reflection.options[:as] # has_many :through against a polymorphic join - jt_conditions << - join_table["#{through_reflection.options[:as]}_type"]. - eq(parent.active_record.base_class.name) - end - 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.foreign_type]. - eq(reflection.options[:source_type]) - else - second_key = source_reflection.foreign_key - end - end - - jt_conditions << - parent_table[jt_primary_key]. - eq(join_table[jt_foreign_key]) - if through_reflection.options[:conditions] - jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name) + def polymorphic_conditions(reflection, table) + if reflection.options[:as] + table[reflection.type]. + eq(reflection.active_record.base_class.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]).and( - 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.foreign_key - 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 |