diff options
Diffstat (limited to 'activerecord/lib')
9 files changed, 722 insertions, 364 deletions
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 911a5155fd..8e7416472f 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -202,87 +202,105 @@ module ActiveRecord set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id') end - def preload_has_one_association(records, reflection, preload_options={}) - return if records.first.send("loaded_#{reflection.name}?") - id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key]) - options = reflection.options - records.each {|record| record.send("set_#{reflection.name}_target", nil)} - if options[:through] - through_records = preload_through_records(records, reflection, options[:through]) - - unless through_records.empty? - through_reflection = reflections[options[:through]] - through_primary_key = through_reflection.primary_key_name - source = reflection.source_reflection.name - through_records.first.class.preload_associations(through_records, source) - if through_reflection.macro == :belongs_to - id_to_record_map = construct_id_map(records, through_primary_key).first - through_primary_key = through_reflection.klass.primary_key - end - - through_records.each do |through_record| - add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s], - reflection.name, through_record.send(source)) - end - end + def preload_has_one_or_has_many_association(records, reflection, preload_options={}) + if reflection.macro == :has_many + return if records.first.send(reflection.name).loaded? + records.each { |record| record.send(reflection.name).loaded } else - set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name) + return if records.first.send("loaded_#{reflection.name}?") + records.each {|record| record.send("set_#{reflection.name}_target", nil)} end - end - def preload_has_many_association(records, reflection, preload_options={}) - return if records.first.send(reflection.name).loaded? options = reflection.options - primary_key_name = reflection.through_reflection_primary_key_name - id_to_record_map, ids = construct_id_map(records, primary_key_name || reflection.options[:primary_key]) - records.each {|record| record.send(reflection.name).loaded} - if options[:through] - through_records = preload_through_records(records, reflection, options[:through]) - unless through_records.empty? + records_with_through_records = preload_through_records(records, reflection, options[:through]) + all_through_records = records_with_through_records.map(&:last).flatten + + unless all_through_records.empty? source = reflection.source_reflection.name - through_records.first.class.preload_associations(through_records, source, options) - through_records.each do |through_record| - through_record_id = through_record[reflection.through_reflection_primary_key].to_s - add_preloaded_records_to_collection(id_to_record_map[through_record_id], reflection.name, through_record.send(source)) + all_through_records.first.class.preload_associations(all_through_records, source, options) + + records_with_through_records.each do |record, through_records| + source_records = through_records.map(&source).flatten.compact + + case reflection.macro + when :has_many, :has_and_belongs_to_many + add_preloaded_records_to_collection([record], reflection.name, source_records) + when :has_one, :belongs_to + add_preloaded_record_to_collection([record], reflection.name, source_records.first) + end end end - else - set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), - reflection.primary_key_name) + id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key]) + associated_records = find_associated_records(ids, reflection, preload_options) + + if reflection.macro == :has_many + set_association_collection_records( + id_to_record_map, reflection.name, + associated_records, reflection.primary_key_name + ) + else + set_association_single_records( + id_to_record_map, reflection.name, + associated_records, reflection.primary_key_name + ) + end end end + alias_method :preload_has_one_association, :preload_has_one_or_has_many_association + alias_method :preload_has_many_association, :preload_has_one_or_has_many_association + def preload_through_records(records, reflection, through_association) + # If the same through record is loaded twice, we want to return exactly the same + # object in the result, rather than two separate instances representing the same + # record. This is so that we can preload the source association for each record, + # and always be able to access the preloaded association regardless of where we + # refer to the record. + # + # Suffices to say, if AR had an identity map built in then this would be unnecessary. + identity_map = {} + + options = {} + if reflection.options[:source_type] interface = reflection.source_reflection.options[:foreign_type] - preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]} - + options[:conditions] = ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]] records.compact! - records.first.class.preload_associations(records, through_association, preload_options) + else + if reflection.options[:conditions] + options[:include] = reflection.options[:include] || + reflection.options[:source] + options[:conditions] = reflection.options[:conditions] + end + + options[:order] = reflection.options[:order] + end - # Dont cache the association - we would only be caching a subset - records.map { |record| + records.first.class.preload_associations(records, through_association, options) + + records.map do |record| + if reflection.options[:source_type] + # Dont cache the association - we would only be caching a subset proxy = record.send(through_association) if proxy.respond_to?(:target) - Array.wrap(proxy.target).tap { proxy.reset } + through_records = proxy.target + proxy.reset else # this is a has_one :through reflection - [proxy].compact + through_records = proxy end - }.flatten(1) - else - options = {} - options[:include] = reflection.options[:include] || reflection.options[:source] if reflection.options[:conditions] - options[:order] = reflection.options[:order] - options[:conditions] = reflection.options[:conditions] - records.first.class.preload_associations(records, through_association, options) + else + through_records = record.send(through_association) + end + + through_records = Array.wrap(through_records).map do |through_record| + identity_map[through_record] ||= through_record + end - records.map { |record| - Array.wrap(record.send(through_association)) - }.flatten(1) + [record, through_records] end end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index c84673e6a6..577a807e3c 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -39,14 +39,6 @@ module ActiveRecord end end - class HasManyThroughSourceAssociationMacroError < ActiveRecordError #:nodoc: - def initialize(reflection) - through_reflection = reflection.through_reflection - source_reflection = reflection.source_reflection - super("Invalid source reflection macro :#{source_reflection.macro}#{" :through" if source_reflection.options[:through]} for has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}. Use :source to specify the source reflection.") - end - end - class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc: def initialize(owner, reflection) super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.") @@ -65,6 +57,12 @@ module ActiveRecord end end + class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc + def initialize(owner, reflection) + super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.") + end + end + class HasAndBelongsToManyAssociationWithPrimaryKeyError < ActiveRecordError #:nodoc: def initialize(reflection) super("Primary key is not allowed in a has_and_belongs_to_many join table (#{reflection.options[:join_table]}).") @@ -113,6 +111,7 @@ module ActiveRecord autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association' autoload :HasOneAssociation, 'active_record/associations/has_one_association' autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association' + autoload :AliasTracker, 'active_record/associations/alias_tracker' # Clears out the association cache. def clear_association_cache #:nodoc: @@ -487,6 +486,49 @@ module ActiveRecord # @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around # @group.avatars.delete(@group.avatars.last) # so would this # + # === Nested Associations + # + # You can actually specify *any* association with the <tt>:through</tt> option, including an + # association which has a <tt>:through</tt> option itself. For example: + # + # class Author < ActiveRecord::Base + # has_many :posts + # has_many :comments, :through => :posts + # has_many :commenters, :through => :comments + # end + # + # class Post < ActiveRecord::Base + # has_many :comments + # end + # + # class Comment < ActiveRecord::Base + # belongs_to :commenter + # end + # + # @author = Author.first + # @author.commenters # => People who commented on posts written by the author + # + # An equivalent way of setting up this association this would be: + # + # class Author < ActiveRecord::Base + # has_many :posts + # has_many :commenters, :through => :posts + # end + # + # class Post < ActiveRecord::Base + # has_many :comments + # has_many :commenters, :through => :comments + # end + # + # class Comment < ActiveRecord::Base + # belongs_to :commenter + # end + # + # When using nested association, you will not be able to modify the association because there + # is not enough information to know what modification to make. For example, if you tried to + # add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the + # intermediate <tt>Post</tt> and <tt>Comment</tt> objects. + # # === Polymorphic Associations # # Polymorphic associations on models are not restricted on what types of models they @@ -934,10 +976,11 @@ module ActiveRecord # [:as] # Specifies a polymorphic interface (See <tt>belongs_to</tt>). # [:through] - # Specifies a join model through which to perform the query. Options for <tt>:class_name</tt> - # and <tt>:foreign_key</tt> are ignored, as the association uses the source reflection. You - # can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>, <tt>has_one</tt> - # or <tt>has_many</tt> association on the join model. The collection of join models + # Specifies a join model through which to perform the query. Options for <tt>:class_name</tt>, + # <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the + # source reflection. You can use a <tt>:through</tt> association through any other, + # association, but if other <tt>:through</tt> associations are involved then the resulting + # association will be read-only. Otherwise, the collection of join models # can be managed via the collection API. For example, new join models are created for # newly associated objects, and if some are gone their rows are deleted (directly, # no destroy callbacks are triggered). @@ -1061,10 +1104,10 @@ module ActiveRecord # you want to do a join but not include the joined columns. Do not forget to include the # primary and foreign keys, otherwise it will raise an error. # [:through] - # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> - # and <tt>:foreign_key</tt> are ignored, as the association uses the source reflection. You - # can only use a <tt>:through</tt> query through a <tt>has_one</tt> or <tt>belongs_to</tt> - # association on the join model. + # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>, + # <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the + # source reflection. You can only use a <tt>:through</tt> query through a <tt>has_one</tt> + # or <tt>belongs_to</tt> association on the join model. # [:source] # Specifies the source association name used by <tt>has_one :through</tt> queries. # Only use it if the name cannot be inferred from the association. @@ -1833,14 +1876,14 @@ module ActiveRecord end class JoinDependency # :nodoc: - attr_reader :join_parts, :reflections, :table_aliases + attr_reader :join_parts, :reflections, :alias_tracker def initialize(base, associations, joins) @join_parts = [JoinBase.new(base, joins)] @associations = {} @reflections = [] - @table_aliases = Hash.new(0) - @table_aliases[base.table_name] = 1 + @alias_tracker = AliasTracker.new(joins) + @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1 build(associations) end @@ -1860,17 +1903,6 @@ module ActiveRecord join_parts.first end - def count_aliases_from_table_joins(name) - # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase - quoted_name = join_base.active_record.connection.quote_table_name(name.downcase).downcase - join_sql = join_base.table_joins.to_s.downcase - join_sql.blank? ? 0 : - # Table names - join_sql.scan(/join(?:\s+\w+)?\s+#{quoted_name}\son/).size + - # Table aliases - join_sql.scan(/join(?:\s+\w+)?\s+\S+\s+#{quoted_name}\son/).size - end - def instantiate(rows) primary_key = join_base.aliased_primary_key parents = {} @@ -2150,11 +2182,11 @@ module ActiveRecord # 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 + attr_reader :aliased_prefix - delegate :options, :through_reflection, :source_reflection, :to => :reflection + delegate :options, :through_reflection, :source_reflection, :through_reflection_chain, :to => :reflection delegate :table, :table_name, :to => :parent, :prefix => true + delegate :alias_tracker, :to => :join_dependency def initialize(reflection, join_dependency, parent = nil) reflection.check_validity! @@ -2165,15 +2197,13 @@ module ActiveRecord super(reflection.klass) - @reflection = reflection - @join_dependency = join_dependency - @parent = parent - @join_type = Arel::InnerJoin + @reflection = reflection + @join_dependency = join_dependency + @parent = parent + @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 + setup_tables end def ==(other) @@ -2189,7 +2219,88 @@ 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.primary_key_name + when :has_many, :has_one + key = reflection.primary_key_name + 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.primary_key_name + 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.primary_key_name + + conditions << source_type_conditions(reflection, foreign_table) + when :has_many, :has_one + key = reflection.primary_key_name + foreign_key = reflection.source_reflection.active_record_primary_key + when :has_and_belongs_to_many + table, join_table = table + + join_key = reflection.primary_key_name + 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) + + relation = relation.join(table, join_type).on(*conditions.flatten.compact) + + # 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) @@ -2198,213 +2309,117 @@ module ActiveRecord end def table - @table ||= Arel::Table.new( - table_name, :as => aliased_table_name, - :engine => arel_engine, :columns => active_record.columns - ) + if @tables.last.is_a?(Array) + @tables.last.first + else + @tables.last + end end - # More semantic name given we are talking about associations - alias_method :target_table, :table + def aliased_table_name + table.table_alias || table.name + end 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 - + def table_alias_for(reflection, join = false) + name = alias_tracker.pluralize(reflection.name) + name << "_#{parent_table_name}" + name << "_join" if join 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 + # 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| + aliased_table_name = alias_tracker.aliased_name_for( + reflection.table_name, + table_alias_for(reflection, reflection != self.reflection) + ) - def process_conditions(conditions, table_name) - Arel.sql(interpolate_sql(sanitize_sql(conditions, table_name))) - end + table = Arel::Table.new( + reflection.table_name, :engine => arel_engine, + :as => aliased_table_name, :columns => reflection.klass.columns + ) - def join_target_table(relation, *conditions) - relation = relation.join(target_table, join_type) + # 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) - # 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] + join_table_name = (reflection.source_reflection || reflection).options[:join_table] - 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 + aliased_join_table_name = alias_tracker.aliased_name_for( + join_table_name, + table_alias_for(reflection, true) + ) - conditions << sti_condition - end + join_table = Arel::Table.new( + join_table_name, :engine => arel_engine, + :as => aliased_join_table_name + ) - # If the reflection has conditions, add them - if options[:conditions] - conditions << process_conditions(options[:conditions], aliased_table_name) + [table, join_table] + else + table + end end - relation = relation.on(*conditions) - 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! - 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]) - ) + @tables 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 reflection_conditions(index, table) + @reflection.through_conditions.reverse[index].map do |condition| + Arel.sql(interpolate_sql(sanitize_sql( + condition, + table.table_alias || table.name + ))) 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 - ) + def sti_conditions(reflection, table) + unless reflection.klass.descends_from_active_record? + sti_column = table[reflection.klass.inheritance_column] - jt_conditions = [] - jt_foreign_key = first_key = second_key = nil + condition = sti_column.eq(reflection.klass.sti_name) - 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 + reflection.klass.descendants.each do |subclass| + condition = condition.or(sti_column.eq(subclass.sti_name)) 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 + condition 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 + def source_type_conditions(reflection, foreign_table) + if reflection.options[:source_type] + foreign_table[reflection.source_reflection.options[:foreign_type]]. + eq(reflection.options[:source_type]) + end + end + + def polymorphic_conditions(reflection, table) + if reflection.options[:as] + table["#{reflection.options[:as]}_type"]. + eq(reflection.active_record.base_class.name) + end + end end + end end end end diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb new file mode 100644 index 0000000000..64582188b6 --- /dev/null +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -0,0 +1,73 @@ +require 'active_support/core_ext/string/conversions' + +module ActiveRecord + module Associations + # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and + # ActiveRecord::Associations::ThroughAssociationScope + class AliasTracker # :nodoc: + # other_sql is some other sql which might conflict with the aliases we assign here. Therefore + # we store other_sql so that we can scan it before assigning a specific name. + def initialize(other_sql = nil) + @aliases = Hash.new + @other_sql = other_sql.to_s.downcase + end + + def aliased_name_for(table_name, aliased_name = nil) + aliased_name ||= table_name + + initialize_count_for(table_name) if @aliases[table_name].nil? + + if @aliases[table_name].zero? + # If it's zero, we can have our table_name + @aliases[table_name] = 1 + table_name + else + # Otherwise, we need to use an alias + aliased_name = connection.table_alias_for(aliased_name) + + initialize_count_for(aliased_name) if @aliases[aliased_name].nil? + + # Update the count + @aliases[aliased_name] += 1 + + if @aliases[aliased_name] > 1 + "#{truncate(aliased_name)}_#{@aliases[aliased_name]}" + else + aliased_name + end + end + end + + def pluralize(table_name) + ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name + end + + private + + def initialize_count_for(name) + @aliases[name] = 0 + + unless @other_sql.blank? + # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase + quoted_name = connection.quote_table_name(name.downcase).downcase + + # Table names + @aliases[name] += @other_sql.scan(/join(?:\s+\w+)?\s+#{quoted_name}\son/).size + + # Table aliases + @aliases[name] += @other_sql.scan(/join(?:\s+\w+)?\s+\S+\s+#{quoted_name}\son/).size + end + + @aliases[name] + end + + def truncate(name) + name[0..connection.table_alias_length-3] + end + + def connection + ActiveRecord::Base.connection + end + end + end +end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 830a82980d..7eaa05ee36 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -7,9 +7,9 @@ module ActiveRecord # is provided by its child HasManyThroughAssociation. class HasManyAssociation < AssociationCollection #:nodoc: protected - def owner_quoted_id - if @reflection.options[:primary_key] - @owner.class.quote_value(@owner.send(@reflection.options[:primary_key])) + def owner_quoted_id(reflection = @reflection) + if reflection.options[:primary_key] + @owner.class.quote_value(@owner.send(reflection.options[:primary_key])) else @owner.quoted_id end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 437c8b1fd6..c45f2ee224 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -7,6 +7,11 @@ module ActiveRecord class HasManyThroughAssociation < HasManyAssociation #:nodoc: include ThroughAssociationScope + def build(attributes = {}, &block) + ensure_not_nested + super + end + alias_method :new, :build def create!(attrs = nil) @@ -36,6 +41,7 @@ module ActiveRecord protected def create_record(attrs, force = true) + ensure_not_nested ensure_owner_is_not_new transaction do @@ -59,6 +65,8 @@ module ActiveRecord end def insert_record(record, force = true, validate = true) + ensure_not_nested + if record.new_record? if force record.save! @@ -74,6 +82,8 @@ module ActiveRecord # TODO - add dependent option support def delete_records(records) + ensure_not_nested + klass = @reflection.through_reflection.klass records.each do |associate| klass.delete_all(construct_join_attributes(associate)) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 17901387e9..c6bcfec275 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -64,9 +64,9 @@ module ActiveRecord end protected - def owner_quoted_id - if @reflection.options[:primary_key] - @owner.class.quote_value(@owner.send(@reflection.options[:primary_key])) + def owner_quoted_id(reflection = @reflection) + if reflection.options[:primary_key] + @owner.class.quote_value(@owner.send(reflection.options[:primary_key])) else @owner.quoted_id end diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index 7f28abf464..e9dc32efd3 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -14,6 +14,8 @@ module ActiveRecord private def create_through_record(new_value) #nodoc: + ensure_not_nested + klass = @reflection.through_reflection.klass current_object = @owner.send(@reflection.through_reflection.name) diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index bd8e304e99..07ce6f1597 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -1,3 +1,5 @@ +require 'enumerator' + module ActiveRecord # = Active Record Through Association Scope module Associations @@ -16,72 +18,246 @@ module ActiveRecord :readonly => @reflection.options[:readonly] } end - + def construct_create_scope - construct_owner_attributes(@reflection) + @reflection.nested? ? {} : construct_owner_attributes(@reflection) end # Build SQL conditions from attributes, qualified by table name. def construct_conditions - table_name = @reflection.through_reflection.quoted_table_name - conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value| - "#{table_name}.#{attr} = #{value}" + reflection = @reflection.through_reflection_chain.last + + if reflection.macro == :has_and_belongs_to_many + table_alias = table_aliases[reflection].first + else + table_alias = table_aliases[reflection] end - conditions << sql_conditions if sql_conditions - "(" + conditions.join(') AND (') + ")" + + parts = construct_quoted_owner_attributes(reflection).map do |attr, value| + "#{table_alias}.#{attr} = #{value}" + end + parts += reflection_conditions(0) + + "(" + parts.join(') AND (') + ")" end # Associate attributes pointing to owner, quoted. def construct_quoted_owner_attributes(reflection) if as = reflection.options[:as] - { "#{as}_id" => owner_quoted_id, + { "#{as}_id" => owner_quoted_id(reflection), "#{as}_type" => reflection.klass.quote_value( @owner.class.base_class.name.to_s, reflection.klass.columns_hash["#{as}_type"]) } elsif reflection.macro == :belongs_to { reflection.klass.primary_key => @owner.class.quote_value(@owner[reflection.primary_key_name]) } else - { reflection.primary_key_name => owner_quoted_id } + { reflection.primary_key_name => owner_quoted_id(reflection) } end end - def construct_from - @reflection.table_name - end - def construct_select(custom_select = nil) distinct = "DISTINCT " if @reflection.options[:uniq] selected = custom_select || @reflection.options[:select] || "#{distinct}#{@reflection.quoted_table_name}.*" end def construct_joins(custom_joins = nil) - polymorphic_join = nil - if @reflection.source_reflection.macro == :belongs_to - reflection_primary_key = @reflection.klass.primary_key - source_primary_key = @reflection.source_reflection.primary_key_name - if @reflection.options[:source_type] - polymorphic_join = "AND %s.%s = %s" % [ - @reflection.through_reflection.quoted_table_name, "#{@reflection.source_reflection.options[:foreign_type]}", - @owner.class.quote_value(@reflection.options[:source_type]) - ] + "#{construct_through_joins} #{@reflection.options[:joins]} #{custom_joins}" + end + + def construct_through_joins + joins, right_index = [], 1 + + # Iterate over each pair in the through reflection chain, joining them together + @reflection.through_reflection_chain.each_cons(2) do |left, right| + right_table_and_alias = table_name_and_alias(right.quoted_table_name, table_aliases[right]) + + if left.source_reflection.nil? + case left.macro + when :belongs_to + joins << inner_join_sql( + right_table_and_alias, + table_aliases[left], left.association_primary_key, + table_aliases[right], left.primary_key_name, + reflection_conditions(right_index) + ) + when :has_many, :has_one + joins << inner_join_sql( + right_table_and_alias, + table_aliases[left], left.primary_key_name, + table_aliases[right], right.association_primary_key, + polymorphic_conditions(left, left), + reflection_conditions(right_index) + ) + when :has_and_belongs_to_many + joins << inner_join_sql( + right_table_and_alias, + table_aliases[left].first, left.primary_key_name, + table_aliases[right], right.klass.primary_key, + reflection_conditions(right_index) + ) + end + else + case left.source_reflection.macro + when :belongs_to + joins << inner_join_sql( + right_table_and_alias, + table_aliases[left], left.association_primary_key, + table_aliases[right], left.primary_key_name, + 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 = table_aliases[right] + right_table_and_alias = table_name_and_alias(right.quoted_table_name, right_table) + else + right_table = table_aliases[right] + end + + joins << inner_join_sql( + right_table_and_alias, + table_aliases[left], left.primary_key_name, + 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_sql( + table_name_and_alias( + quote_table_name(right.options[:join_table]), + 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 = table_aliases[left] + + joins << inner_join_sql( + table_name_and_alias( + quote_table_name(left.source_reflection.options[:join_table]), + join_table + ), + left_table, left.klass.primary_key, + join_table, left.association_foreign_key + ) + + joins << inner_join_sql( + right_table_and_alias, + join_table, left.primary_key_name, + table_aliases[right], right.klass.primary_key, + reflection_conditions(right_index) + ) + end end - else - reflection_primary_key = @reflection.source_reflection.primary_key_name - source_primary_key = @reflection.through_reflection.klass.primary_key - if @reflection.source_reflection.options[:as] - polymorphic_join = "AND %s.%s = %s" % [ - @reflection.quoted_table_name, "#{@reflection.source_reflection.options[:as]}_type", - @owner.class.quote_value(@reflection.through_reflection.klass.name) - ] + + right_index += 1 + end + + joins.join(" ") + end + + def alias_tracker + @alias_tracker ||= AliasTracker.new + end + + def table_aliases + @table_aliases ||= begin + @reflection.through_reflection_chain.inject({}) do |aliases, reflection| + table_alias = quote_table_name(alias_tracker.aliased_name_for( + reflection.table_name, + table_alias_for(reflection, reflection != @reflection) + )) + + if reflection.macro == :has_and_belongs_to_many || + (reflection.source_reflection && + reflection.source_reflection.macro == :has_and_belongs_to_many) + + join_table_alias = quote_table_name(alias_tracker.aliased_name_for( + (reflection.source_reflection || reflection).options[:join_table], + table_alias_for(reflection, true) + )) + + aliases[reflection] = [join_table_alias, table_alias] + else + aliases[reflection] = table_alias + end + + aliases end end + end + + def table_alias_for(reflection, join = false) + name = alias_tracker.pluralize(reflection.name) + name << "_#{@reflection.name}" + name << "_join" if join + name + end + + def quote_table_name(table_name) + @reflection.klass.connection.quote_table_name(table_name) + end - "INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [ - @reflection.through_reflection.quoted_table_name, - @reflection.quoted_table_name, reflection_primary_key, - @reflection.through_reflection.quoted_table_name, source_primary_key, - polymorphic_join - ] + def table_name_and_alias(table_name, table_alias) + "#{table_name} #{table_alias if table_alias != table_name}".strip + end + + def inner_join_sql(table, on_left_table, on_left_key, on_right_table, on_right_key, *conditions) + conditions << "#{on_left_table}.#{on_left_key} = #{on_right_table}.#{on_right_key}" + conditions = conditions.flatten.compact + conditions = conditions.map { |sql| "(#{sql})" } * ' AND ' + + "INNER JOIN #{table} ON #{conditions}" + end + + def reflection_conditions(index) + reflection = @reflection.through_reflection_chain[index] + reflection_conditions = @reflection.through_conditions[index] + + conditions = [] + + if reflection.options[:as].nil? && # reflection.klass is a Module if :as is used + reflection.klass.finder_needs_type_condition? + conditions << reflection.klass.send(:type_condition).to_sql + end + + reflection_conditions.each do |condition| + sanitized_condition = reflection.klass.send(:sanitize_sql, condition) + interpolated_condition = interpolate_sql(sanitized_condition) + + if condition.is_a?(Hash) + interpolated_condition.gsub!( + @reflection.quoted_table_name, + reflection.quoted_table_name + ) + end + + conditions << interpolated_condition + end + + conditions + end + + def polymorphic_conditions(reflection, polymorphic_reflection) + if polymorphic_reflection.options[:as] + "%s.%s = %s" % [ + table_aliases[reflection], "#{polymorphic_reflection.options[:as]}_type", + @owner.class.quote_value(polymorphic_reflection.active_record.base_class.name) + ] + end + end + + def source_type_conditions(reflection) + if reflection.options[:source_type] + "%s.%s = %s" % [ + table_aliases[reflection.through_reflection], + reflection.source_reflection.options[:foreign_type].to_s, + @owner.class.quote_value(reflection.options[:source_type]) + ] + end end # Construct attributes for associate pointing to owner. @@ -95,6 +271,8 @@ module ActiveRecord end # Construct attributes for :through pointing to owner and associate. + # This method is used when adding records to the association. Since this only makes sense for + # non-nested through associations, that's the only case we have to worry about here. 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) @@ -112,47 +290,11 @@ module ActiveRecord join_attributes end - def conditions - @conditions = build_conditions unless defined?(@conditions) - @conditions - end - - def build_conditions - association_conditions = @reflection.options[:conditions] - through_conditions = build_through_conditions - source_conditions = @reflection.source_reflection.options[:conditions] - uses_sti = !@reflection.through_reflection.klass.descends_from_active_record? - - if association_conditions || through_conditions || source_conditions || uses_sti - all = [] - - [association_conditions, source_conditions].each do |conditions| - all << interpolate_sql(sanitize_sql(conditions)) if conditions - end - - all << through_conditions if through_conditions - all << build_sti_condition if uses_sti - - all.map { |sql| "(#{sql})" } * ' AND ' - end - end - - def build_through_conditions - conditions = @reflection.through_reflection.options[:conditions] - if conditions.is_a?(Hash) - interpolate_sql(@reflection.through_reflection.klass.send(:sanitize_sql, conditions)).gsub( - @reflection.quoted_table_name, - @reflection.through_reflection.quoted_table_name) - elsif conditions - interpolate_sql(sanitize_sql(conditions)) + def ensure_not_nested + if @reflection.nested? + raise HasManyThroughNestedAssociationsAreReadonly.new(@owner, @reflection) end end - - def build_sti_condition - @reflection.through_reflection.klass.send(:type_condition).to_sql - end - - alias_method :sql_conditions, :conditions end end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index a2260e9a19..ba37fed3c7 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -210,6 +210,14 @@ module ActiveRecord @association_foreign_key ||= @options[:association_foreign_key] || class_name.foreign_key end + def association_primary_key + @association_primary_key ||= @options[:primary_key] || klass.primary_key + end + + def active_record_primary_key + @active_record_primary_key ||= @options[:primary_key] || active_record.primary_key + end + def counter_cache_column if options[:counter_cache] == true "#{active_record.name.demodulize.underscore.pluralize}_count" @@ -242,7 +250,12 @@ module ActiveRecord false end - def through_reflection_primary_key_name + def through_reflection_chain + [self] + end + + def through_conditions + [Array.wrap(options[:conditions])] end def source_reflection @@ -326,6 +339,8 @@ module ActiveRecord # Holds all the meta-data about a :through association as it was specified # in the Active Record class. class ThroughReflection < AssociationReflection #:nodoc: + delegate :primary_key_name, :association_foreign_key, :to => :source_reflection + # Gets the source of the through reflection. It checks both a singularized # and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>. # @@ -353,6 +368,101 @@ module ActiveRecord @through_reflection ||= active_record.reflect_on_association(options[:through]) end + # Returns an array of AssociationReflection objects which are involved in this through + # association. Each item in the array corresponds to a table which will be part of the + # query for this association. + # + # If the source reflection is itself a ThroughReflection, then we don't include self in + # the chain, but just defer to the source reflection. + # + # The chain is built by recursively calling through_reflection_chain on the source + # reflection and the through reflection. The base case for the recursion is a normal + # association, which just returns [self] for its through_reflection_chain. + def through_reflection_chain + @through_reflection_chain ||= begin + if source_reflection.source_reflection + # If the source reflection has its own source reflection, then the chain must start + # by getting us to that source reflection. + chain = source_reflection.through_reflection_chain + else + # If the source reflection does not go through another reflection, then we can get + # to this reflection directly, and so start the chain here + chain = [self] + end + + # Recursively build the rest of the chain + chain += through_reflection.through_reflection_chain + + # Finally return the completed chain + chain + end + end + + # Consider the following example: + # + # class Person + # has_many :articles + # has_many :comment_tags, :through => :articles + # end + # + # class Article + # has_many :comments + # has_many :comment_tags, :through => :comments, :source => :tags + # end + # + # class Comment + # has_many :tags + # end + # + # There may be conditions on Person.comment_tags, Article.comment_tags and/or Comment.tags, + # but only Comment.tags will be represented in the through_reflection_chain. So this method + # creates an array of conditions corresponding to the through_reflection_chain. Each item in + # the through_conditions array corresponds to an item in the through_reflection_chain, and is + # itself an array of conditions from an arbitrary number of relevant reflections. + def through_conditions + @through_conditions ||= begin + # Initialize the first item - which corresponds to this reflection - either by recursing + # into the souce reflection (if it is itself a through reflection), or by grabbing the + # source reflection conditions. + if source_reflection.source_reflection + conditions = source_reflection.through_conditions + else + conditions = [Array.wrap(source_reflection.options[:conditions])] + end + + # Add to it the conditions from this reflection if necessary. + conditions.first << options[:conditions] if options[:conditions] + + # Recursively fill out the rest of the array from the through reflection + conditions += through_reflection.through_conditions + + # And return + conditions + end + end + + # A through association is nested iff there would be more than one join table + def nested? + through_reflection_chain.length > 2 || + through_reflection.macro == :has_and_belongs_to_many + end + + # We want to use the klass from this reflection, rather than just delegate straight to + # the source_reflection, because the source_reflection may be polymorphic. We still + # need to respect the source_reflection's :primary_key option, though. + def association_primary_key + @association_primary_key ||= begin + # Get the "actual" source reflection if the immediate source reflection has a + # source reflection itself + source_reflection = self.source_reflection + while source_reflection.source_reflection + source_reflection = source_reflection.source_reflection + end + + source_reflection.options[:primary_key] || klass.primary_key + end + end + # Gets an array of possible <tt>:through</tt> source reflection names: # # [:singularized, :pluralized] @@ -378,21 +488,9 @@ module ActiveRecord raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection) end - unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil? - raise HasManyThroughSourceAssociationMacroError.new(self) - end - check_validity_of_inverse! end - def through_reflection_primary_key - through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.primary_key_name - end - - def through_reflection_primary_key_name - through_reflection.primary_key_name if through_reflection.belongs_to? - end - private def derive_class_name # get the class_name of the belongs_to association of the through reflection |