aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record')
-rw-r--r--activerecord/lib/active_record/association_preload.rb140
-rw-r--r--activerecord/lib/active_record/associations.rb481
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb73
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb10
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/through_association_scope.rb296
-rw-r--r--activerecord/lib/active_record/reflection.rb126
9 files changed, 749 insertions, 391 deletions
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb
index 911a5155fd..c3ccb93ffd 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
+
+ records.first.class.preload_associations(records, through_association, options)
- # Dont cache the association - we would only be caching a subset
- records.map { |record|
+ 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)
-
- records.map { |record|
- Array.wrap(record.send(through_association))
- }.flatten(1)
+ 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
+
+ [record, through_records]
end
end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index dc466eafc2..37dbff4061 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}.")
@@ -64,6 +56,12 @@ module ActiveRecord
super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.")
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)
@@ -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,7 +1876,7 @@ 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)]
@@ -1841,8 +1884,8 @@ module ActiveRecord
@reflections = []
@base_records_hash = {}
@base_records_in_order = []
- @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
@@ -1862,17 +1905,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)
rows.each_with_index do |row, i|
primary_id = join_base.record_id(row)
@@ -2151,12 +2183,12 @@ 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
-
- delegate :options, :through_reflection, :source_reflection, :to => :reflection
+
+ attr_reader :aliased_prefix
+
+ 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!
@@ -2166,16 +2198,14 @@ module ActiveRecord
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
+
+ @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)
@@ -2191,7 +2221,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)
@@ -2200,213 +2311,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
-
- 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))
+
+ # 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)
+ )
+
+ table = Arel::Table.new(
+ reflection.table_name, :engine => arel_engine,
+ :as => aliased_table_name, :columns => reflection.klass.columns
+ )
+
+ # 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_name = (reflection.source_reflection || reflection).options[:join_table]
+
+ aliased_join_table_name = alias_tracker.aliased_name_for(
+ join_table_name,
+ table_alias_for(reflection, true)
+ )
+
+ join_table = Arel::Table.new(
+ join_table_name, :engine => arel_engine,
+ :as => aliased_join_table_name
+ )
+
+ [table, join_table]
+ else
+ table
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])
- )
+
+ # 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!
+
+ @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
- )
-
- 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
+
+ def sti_conditions(reflection, table)
+ unless reflection.klass.descends_from_active_record?
+ sti_column = table[reflection.klass.inheritance_column]
+
+ condition = sti_column.eq(reflection.klass.sti_name)
+
+ reflection.klass.descendants.each do |subclass|
+ condition = condition.or(sti_column.eq(subclass.sti_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..10e90ec117
--- /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..2c9fa3b447 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..de962e01b6 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..abe7af418d 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
@@ -18,70 +20,244 @@ module ActiveRecord
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])
- ]
- 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)
- ]
+ "#{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
+
+ right_index += 1
end
+
+ joins.join(" ")
+ 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 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
+
+ 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)
@@ -111,48 +289,12 @@ 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..6eb2057f66 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -209,6 +209,14 @@ module ActiveRecord
def association_foreign_key
@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
@@ -241,8 +249,13 @@ module ActiveRecord
def through_reflection
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>.
#
@@ -352,6 +367,101 @@ module ActiveRecord
def through_reflection
@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:
#
@@ -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