aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib')
-rw-r--r--activerecord/lib/active_record/associations.rb72
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb85
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb32
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb334
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb5
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb221
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb11
-rw-r--r--activerecord/lib/active_record/reflection.rb116
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb3
11 files changed, 605 insertions, 282 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index e91cbd7f33..ec5b41a3e7 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -52,14 +52,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}.")
@@ -78,6 +70,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]}).")
@@ -144,6 +142,7 @@ module ActiveRecord
autoload :Preloader, 'active_record/associations/preloader'
autoload :JoinDependency, 'active_record/associations/join_dependency'
+ autoload :AliasTracker, 'active_record/associations/alias_tracker'
# Clears out the association cache.
def clear_association_cache #:nodoc:
@@ -548,6 +547,49 @@ module ActiveRecord
# belongs_to :tag, :inverse_of => :taggings
# end
#
+ # === 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
@@ -1068,10 +1110,10 @@ 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>,
+ # Specifies an association through which to perform the query. This can be any other type
+ # of association, including other <tt>:through</tt> associations. 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>belongs_to</tt>,
- # <tt>has_one</tt> or <tt>has_many</tt> association on the join model.
+ # source reflection.
#
# If the association on the join model is a +belongs_to+, the collection can be modified
# and the records on the <tt>:through</tt> model will be automatically created and removed
@@ -1198,10 +1240,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.
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..6fc2bfdb31
--- /dev/null
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -0,0 +1,85 @@
+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:
+ attr_reader :aliases, :table_joins
+
+ # table_joins is an array of arel joins which might conflict with the aliases we assign here
+ def initialize(table_joins = [])
+ @aliases = Hash.new
+ @table_joins = table_joins
+ end
+
+ def aliased_table_for(table_name, aliased_name = nil)
+ table_alias = aliased_name_for(table_name, aliased_name)
+
+ if table_alias == table_name # TODO: Is this conditional necessary?
+ Arel::Table.new(table_name)
+ else
+ Arel::Table.new(table_name).alias(table_alias)
+ end
+ 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 Arel::Table === table_joins
+ # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
+ quoted_name = connection.quote_table_name(name).downcase
+
+ aliases[name] += table_joins.map { |join|
+ # Table names + table aliases
+ join.left.downcase.scan(
+ /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
+ ).size
+ }.sum
+ 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_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index acac68fda5..9d2b29685b 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -34,7 +34,9 @@ module ActiveRecord
end
def insert_record(record, validate = true)
+ ensure_not_nested
return if record.new_record? && !record.save(:validate => validate)
+
through_record(record).save!
update_counter(1)
record
@@ -59,6 +61,8 @@ module ActiveRecord
end
def build_record(attributes)
+ ensure_not_nested
+
record = super(attributes)
inverse = source_reflection.inverse_of
@@ -93,6 +97,8 @@ module ActiveRecord
end
def delete_records(records, method)
+ ensure_not_nested
+
through = owner.association(through_reflection.name)
scope = through.scoped.where(construct_join_attributes(*records))
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 d76d729303..fdf8ae1453 100644
--- a/activerecord/lib/active_record/associations/has_one_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -12,6 +12,8 @@ module ActiveRecord
private
def create_through_record(record)
+ ensure_not_nested
+
through_proxy = owner.association(through_reflection.name)
through_record = through_proxy.send(:load_target)
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index c7c3cf521c..504f25271c 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -5,18 +5,16 @@ module ActiveRecord
autoload :JoinBase, 'active_record/associations/join_dependency/join_base'
autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association'
- attr_reader :join_parts, :reflections, :table_aliases, :active_record
+ attr_reader :join_parts, :reflections, :alias_tracker, :active_record
def initialize(base, associations, joins)
- @active_record = base
- @table_joins = joins
- @join_parts = [JoinBase.new(base)]
- @associations = {}
- @reflections = []
- @table_aliases = Hash.new do |h,name|
- h[name] = count_aliases_from_table_joins(name.downcase)
- end
- @table_aliases[base.table_name] = 1
+ @active_record = base
+ @table_joins = joins
+ @join_parts = [JoinBase.new(base)]
+ @associations = {}
+ @reflections = []
+ @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
@@ -45,20 +43,6 @@ module ActiveRecord
}.flatten
end
- def count_aliases_from_table_joins(name)
- return 0 if Arel::Table === @table_joins
-
- # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
- quoted_name = active_record.connection.quote_table_name(name).downcase
-
- @table_joins.map { |join|
- # Table names + table aliases
- join.left.downcase.scan(
- /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
- ).size
- }.sum
- end
-
def instantiate(rows)
primary_key = join_base.aliased_primary_key
parents = {}
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
index ebe39c35fe..890e77fca9 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -18,10 +18,13 @@ module ActiveRecord
attr_accessor :join_type
# These implement abstract methods from the superclass
- attr_reader :aliased_prefix, :aliased_table_name
+ attr_reader :aliased_prefix
- delegate :options, :through_reflection, :source_reflection, :to => :reflection
+ attr_reader :tables
+
+ delegate :options, :through_reflection, :source_reflection, :through_reflection_chain, :to => :reflection
delegate :table, :table_name, :to => :parent, :prefix => :parent
+ delegate :alias_tracker, :to => :join_dependency
def initialize(reflection, join_dependency, parent = nil)
reflection.check_validity!
@@ -38,13 +41,7 @@ module ActiveRecord
@join_type = Arel::InnerJoin
@aliased_prefix = "t#{ join_dependency.join_parts.size }"
- # This must be done eagerly upon initialisation because the alias which is produced
- # depends on the state of the join dependency, but we want it to work the same way
- # every time.
- allocate_aliases
- @table = Arel::Table.new(
- table_name, :as => aliased_table_name, :engine => arel_engine
- )
+ setup_tables
end
def ==(other)
@@ -60,7 +57,95 @@ module ActiveRecord
end
def join_to(relation)
- send("join_#{reflection.macro}_to", relation)
+ # The chain starts with the target table, but we want to end with it here (makes
+ # more sense in this context)
+ chain = through_reflection_chain.reverse
+
+ foreign_table = parent_table
+ index = 0
+
+ chain.each do |reflection|
+ table = tables[index]
+ conditions = []
+
+ if reflection.source_reflection.nil?
+ case reflection.macro
+ when :belongs_to
+ key = reflection.association_primary_key
+ foreign_key = reflection.foreign_key
+ when :has_many, :has_one
+ key = reflection.foreign_key
+ foreign_key = reflection.active_record_primary_key
+
+ conditions << polymorphic_conditions(reflection, table)
+ when :has_and_belongs_to_many
+ # For habtm, we need to deal with the join table at the same time as the
+ # target table (because unlike a :through association, there is no reflection
+ # to represent the join table)
+ table, join_table = table
+
+ join_key = reflection.foreign_key
+ join_foreign_key = reflection.active_record.primary_key
+
+ relation = relation.join(join_table, join_type).on(
+ join_table[join_key].
+ eq(foreign_table[join_foreign_key])
+ )
+
+ # We've done the first join now, so update the foreign_table for the second
+ foreign_table = join_table
+
+ key = reflection.klass.primary_key
+ foreign_key = reflection.association_foreign_key
+ end
+ else
+ case reflection.source_reflection.macro
+ when :belongs_to
+ key = reflection.association_primary_key
+ foreign_key = reflection.foreign_key
+
+ conditions << source_type_conditions(reflection, foreign_table)
+ when :has_many, :has_one
+ key = reflection.foreign_key
+ foreign_key = reflection.source_reflection.active_record_primary_key
+ when :has_and_belongs_to_many
+ table, join_table = table
+
+ join_key = reflection.foreign_key
+ join_foreign_key = reflection.klass.primary_key
+
+ relation = relation.join(join_table, join_type).on(
+ join_table[join_key].
+ eq(foreign_table[join_foreign_key])
+ )
+
+ foreign_table = join_table
+
+ key = reflection.klass.primary_key
+ foreign_key = reflection.association_foreign_key
+ end
+ end
+
+ conditions << table[key].eq(foreign_table[foreign_key])
+
+ conditions << reflection_conditions(index, table)
+ conditions << sti_conditions(reflection, table)
+
+ ands = relation.create_and(conditions.flatten.compact)
+
+ join = relation.create_join(
+ table,
+ relation.create_on(ands),
+ join_type)
+
+ relation = relation.from(join)
+
+ # The current table in this iteration becomes the foreign table in the next
+ foreign_table = table
+ index += 1
+ end
+
+ relation
end
def join_relation(joining_relation)
@@ -68,42 +153,59 @@ module ActiveRecord
joining_relation.joins(self)
end
- attr_reader :table
- # More semantic name given we are talking about associations
- alias_method :target_table, :table
-
- protected
-
- def aliased_table_name_for(name, suffix = nil)
- aliases = @join_dependency.table_aliases
-
- if aliases[name] != 0 # We need an alias
- connection = active_record.connection
-
- name = connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
- aliases[name] += 1
- name = name[0, connection.table_alias_length-3] + "_#{aliases[name]}" if aliases[name] > 1
+ def table
+ if tables.last.is_a?(Array)
+ tables.last.first
else
- aliases[name] += 1
+ tables.last
end
+ end
- name
+ def aliased_table_name
+ table.table_alias || table.name
end
- def pluralize(table_name)
- ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
+ protected
+
+ def table_alias_for(reflection, join = false)
+ name = alias_tracker.pluralize(reflection.name)
+ name << "_#{parent_table_name}"
+ name << "_join" if join
+ name
end
private
- def allocate_aliases
- @aliased_table_name = aliased_table_name_for(table_name)
+ # Generate aliases and Arel::Table instances for each of the tables which we will
+ # later generate joins for. We must do this in advance in order to correctly allocate
+ # the proper alias.
+ def setup_tables
+ @tables = through_reflection_chain.map do |reflection|
+ table = alias_tracker.aliased_table_for(
+ reflection.table_name,
+ table_alias_for(reflection, reflection != self.reflection)
+ )
+
+ # For habtm, we have two Arel::Table instances related to a single reflection, so
+ # we just store them as a pair in the array.
+ if reflection.macro == :has_and_belongs_to_many ||
+ (reflection.source_reflection && reflection.source_reflection.macro == :has_and_belongs_to_many)
+
+ join_table = alias_tracker.aliased_table_for(
+ (reflection.source_reflection || reflection).options[:join_table],
+ table_alias_for(reflection, true)
+ )
- if reflection.macro == :has_and_belongs_to_many
- @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
- elsif [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
- @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
+ [table, join_table]
+ else
+ table
+ end
end
+
+ # The joins are generated from the through_reflection_chain in reverse order, so
+ # reverse the tables too (but it's important to generate the aliases in the 'forward'
+ # order, which is why we only do the reversal now.
+ @tables.reverse!
end
def process_conditions(conditions, table_name)
@@ -118,161 +220,39 @@ module ActiveRecord
active_record.send(:sanitize_sql, condition, table_name)
end
- def join_target_table(relation, condition)
- conditions = [condition]
+ def reflection_conditions(index, table)
+ reflection.through_conditions.reverse[index].map do |condition|
+ process_conditions(condition, table.table_alias || table.name)
+ end
+ end
- # If the target table is an STI model then we must be sure to only include records of
- # its type and its sub-types.
- unless active_record.descends_from_active_record?
- sti_column = target_table[active_record.inheritance_column]
- subclasses = active_record.descendants
- sti_condition = sti_column.eq(active_record.sti_name)
+ def sti_conditions(reflection, table)
+ unless reflection.klass.descends_from_active_record?
+ sti_column = table[reflection.klass.inheritance_column]
+ sti_condition = sti_column.eq(reflection.klass.sti_name)
+ subclasses = reflection.klass.descendants
- conditions << subclasses.inject(sti_condition) { |attr,subclass|
+ # TODO: use IN (...), or possibly AR::Base#type_condition
+ subclasses.inject(sti_condition) { |attr,subclass|
attr.or(sti_column.eq(subclass.sti_name))
}
end
-
- # If the reflection has conditions, add them
- if options[:conditions]
- conditions << process_conditions(options[:conditions], aliased_table_name)
- end
-
- ands = relation.create_and(conditions)
-
- join = relation.create_join(
- target_table,
- relation.create_on(ands),
- join_type)
-
- relation.from join
- end
-
- def join_has_and_belongs_to_many_to(relation)
- join_table = Arel::Table.new(
- options[:join_table]
- ).alias(@aliased_join_table_name)
-
- fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key
- klass_fk = options[:association_foreign_key] || reflection.klass.to_s.foreign_key
-
- relation = relation.join(join_table, join_type)
- relation = relation.on(
- join_table[fk].
- eq(parent_table[reflection.active_record.primary_key])
- )
-
- join_target_table(
- relation,
- target_table[reflection.klass.primary_key].
- eq(join_table[klass_fk])
- )
end
- def join_has_many_to(relation)
- if reflection.options[:through]
- join_has_many_through_to(relation)
- elsif reflection.options[:as]
- join_has_many_polymorphic_to(relation)
- else
- foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
- primary_key = options[:primary_key] || parent.primary_key
-
- join_target_table(
- relation,
- target_table[foreign_key].
- eq(parent_table[primary_key])
- )
+ def source_type_conditions(reflection, foreign_table)
+ if reflection.options[:source_type]
+ foreign_table[reflection.source_reflection.foreign_type].
+ eq(reflection.options[:source_type])
end
end
- alias :join_has_one_to :join_has_many_to
-
- def join_has_many_through_to(relation)
- join_table = Arel::Table.new(
- through_reflection.klass.table_name
- ).alias @aliased_join_table_name
-
- jt_conditions = []
- first_key = second_key = nil
-
- if through_reflection.macro == :belongs_to
- jt_primary_key = through_reflection.foreign_key
- jt_foreign_key = through_reflection.association_primary_key
- else
- jt_primary_key = through_reflection.active_record_primary_key
- jt_foreign_key = through_reflection.foreign_key
-
- if through_reflection.options[:as] # has_many :through against a polymorphic join
- jt_conditions <<
- join_table["#{through_reflection.options[:as]}_type"].
- eq(parent.active_record.base_class.name)
- end
- end
-
- case source_reflection.macro
- when :has_many
- second_key = options[:foreign_key] || primary_key
-
- if source_reflection.options[:as]
- first_key = "#{source_reflection.options[:as]}_id"
- else
- first_key = through_reflection.klass.base_class.to_s.foreign_key
- end
-
- unless through_reflection.klass.descends_from_active_record?
- jt_conditions <<
- join_table[through_reflection.active_record.inheritance_column].
- eq(through_reflection.klass.sti_name)
- end
- when :belongs_to
- first_key = primary_key
-
- if reflection.options[:source_type]
- second_key = source_reflection.association_foreign_key
-
- jt_conditions <<
- join_table[reflection.source_reflection.foreign_type].
- eq(reflection.options[:source_type])
- else
- second_key = source_reflection.foreign_key
- end
- end
-
- jt_conditions <<
- parent_table[jt_primary_key].
- eq(join_table[jt_foreign_key])
- if through_reflection.options[:conditions]
- jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name)
+ def polymorphic_conditions(reflection, table)
+ if reflection.options[:as]
+ table[reflection.type].
+ eq(reflection.active_record.base_class.name)
end
-
- relation = relation.join(join_table, join_type).on(*jt_conditions)
-
- join_target_table(
- relation,
- target_table[first_key].eq(join_table[second_key])
- )
end
- def join_has_many_polymorphic_to(relation)
- join_target_table(
- relation,
- target_table["#{reflection.options[:as]}_id"].
- eq(parent_table[parent.primary_key]).and(
- target_table["#{reflection.options[:as]}_type"].
- eq(parent.active_record.base_class.name))
- )
- end
-
- def join_belongs_to_to(relation)
- foreign_key = options[:foreign_key] || reflection.foreign_key
- primary_key = options[:primary_key] || reflection.klass.primary_key
-
- join_target_table(
- relation,
- target_table[primary_key].eq(parent_table[foreign_key])
- )
- end
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index d630fc4c63..ad6374d09a 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -19,8 +19,9 @@ module ActiveRecord
source_reflection.name, options
).run
- through_records.each do |owner, owner_through_records|
- owner_through_records.map! { |r| r.send(source_reflection.name) }.flatten!
+ through_records.each do |owner, records|
+ records.map! { |r| r.send(source_reflection.name) }.flatten!
+ records.compact!
end
end
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index e1d60ccb17..ed24373cba 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -1,9 +1,12 @@
+require 'enumerator'
+
module ActiveRecord
# = Active Record Through Association
module Associations
module ThroughAssociation #:nodoc:
- delegate :source_options, :through_options, :source_reflection, :through_reflection, :to => :reflection
+ delegate :source_options, :through_options, :source_reflection, :through_reflection,
+ :through_reflection_chain, :through_conditions, :to => :reflection
protected
@@ -13,10 +16,12 @@ module ActiveRecord
def association_scope
scope = super.joins(construct_joins)
- scope = add_conditions(scope)
+ scope = scope.where(reflection_conditions(0))
+
unless options[:include]
scope = scope.includes(source_options[:include])
end
+
scope
end
@@ -30,6 +35,7 @@ module ActiveRecord
{ }
end
+ # TODO: Needed?
def aliased_through_table
name = through_reflection.table_name
@@ -39,41 +45,101 @@ module ActiveRecord
end
def construct_owner_conditions
- super(aliased_through_table, through_reflection)
+ reflection = through_reflection_chain.last
+
+ if reflection.macro == :has_and_belongs_to_many
+ table = tables[reflection].first
+ else
+ table = Array.wrap(tables[reflection]).first
+ end
+
+ super(table, reflection)
end
def construct_joins
- right = aliased_through_table
- left = reflection.klass.arel_table
+ joins, right_index = [], 1
- conditions = []
+ # Iterate over each pair in the through reflection chain, joining them together
+ through_reflection_chain.each_cons(2) do |left, right|
+ left_table, right_table = tables[left], tables[right]
- if source_reflection.macro == :belongs_to
- reflection_primary_key = source_reflection.association_primary_key
- source_primary_key = source_reflection.foreign_key
+ if left.source_reflection.nil?
+ case left.macro
+ when :belongs_to
+ joins << inner_join(
+ right_table,
+ left_table[left.association_primary_key],
+ right_table[left.foreign_key],
+ reflection_conditions(right_index)
+ )
+ when :has_many, :has_one
+ joins << inner_join(
+ right_table,
+ left_table[left.foreign_key],
+ right_table[right.association_primary_key],
+ polymorphic_conditions(left, left),
+ reflection_conditions(right_index)
+ )
+ when :has_and_belongs_to_many
+ joins << inner_join(
+ right_table,
+ left_table.first[left.foreign_key],
+ right_table[right.klass.primary_key],
+ reflection_conditions(right_index)
+ )
+ end
+ else
+ case left.source_reflection.macro
+ when :belongs_to
+ joins << inner_join(
+ right_table,
+ left_table[left.association_primary_key],
+ right_table[left.foreign_key],
+ 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 = tables[right]
+ end
- if options[:source_type]
- column = source_reflection.foreign_type
- conditions <<
- right[column].eq(options[:source_type])
- end
- else
- reflection_primary_key = source_reflection.foreign_key
- source_primary_key = source_reflection.active_record_primary_key
+ joins << inner_join(
+ right_table,
+ left_table[left.foreign_key],
+ right_table[left.source_reflection.active_record_primary_key],
+ polymorphic_conditions(left, left.source_reflection),
+ reflection_conditions(right_index)
+ )
- if source_options[:as]
- column = "#{source_options[:as]}_type"
- conditions <<
- left[column].eq(through_reflection.klass.name)
+ if right.macro == :has_and_belongs_to_many
+ joins << inner_join(
+ 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 = tables[left]
+
+ joins << inner_join(
+ join_table,
+ left_table[left.klass.primary_key],
+ join_table[left.association_foreign_key]
+ )
+
+ joins << inner_join(
+ right_table,
+ join_table[left.foreign_key],
+ right_table[right.klass.primary_key],
+ reflection_conditions(right_index)
+ )
+ end
end
- end
- conditions <<
- left[reflection_primary_key].eq(right[source_primary_key])
+ right_index += 1
+ end
- right.create_join(
- right,
- right.create_on(right.create_and(conditions)))
+ joins
end
# Construct attributes for :through pointing to owner and associate. This is used by the
@@ -112,37 +178,88 @@ module ActiveRecord
end
end
- # The reason that we are operating directly on the scope here (rather than passing
- # back some arel conditions to be added to the scope) is because scope.where([x, y])
- # has a different meaning to scope.where(x).where(y) - the first version might
- # perform some substitution if x is a string.
- def add_conditions(scope)
- unless through_reflection.klass.descends_from_active_record?
- scope = scope.where(through_reflection.klass.send(:type_condition))
+ def alias_tracker
+ @alias_tracker ||= AliasTracker.new
+ end
+
+ # TODO: It is decidedly icky to have an array for habtm entries, and no array for others
+ def tables
+ @tables ||= begin
+ Hash[
+ through_reflection_chain.map do |reflection|
+ table = alias_tracker.aliased_table_for(
+ reflection.table_name,
+ table_alias_for(reflection, reflection != self.reflection)
+ )
+
+ if reflection.macro == :has_and_belongs_to_many ||
+ (reflection.source_reflection &&
+ reflection.source_reflection.macro == :has_and_belongs_to_many)
+
+ join_table = alias_tracker.aliased_table_for(
+ (reflection.source_reflection || reflection).options[:join_table],
+ table_alias_for(reflection, true)
+ )
+
+ [reflection, [join_table, table]]
+ else
+ [reflection, table]
+ end
+ end
+ ]
end
+ end
- scope = scope.where(interpolate(source_options[:conditions]))
- scope.where(through_conditions)
+ def table_alias_for(reflection, join = false)
+ name = alias_tracker.pluralize(reflection.name)
+ name << "_#{self.reflection.name}"
+ name << "_join" if join
+ name
end
- # If there is a hash of conditions then we make sure the keys are scoped to the
- # through table name if left ambiguous.
- def through_conditions
- conditions = interpolate(through_options[:conditions])
+ def inner_join(table, left_column, right_column, *conditions)
+ conditions << left_column.eq(right_column)
- if conditions.is_a?(Hash)
- Hash[conditions.map { |key, value|
- unless value.is_a?(Hash) || key.to_s.include?('.')
- key = aliased_through_table.name + '.' + key.to_s
- end
+ table.create_join(
+ table,
+ table.create_on(table.create_and(conditions.flatten.compact)))
+ end
- [key, value]
- }]
- else
- conditions
+ def reflection_conditions(index)
+ reflection = through_reflection_chain[index]
+ conditions = through_conditions[index].dup
+
+ # TODO: maybe this should go in Reflection#through_conditions directly?
+ unless reflection.klass.descends_from_active_record?
+ conditions << reflection.klass.send(:type_condition)
+ end
+
+ unless conditions.empty?
+ conditions.map! do |condition|
+ condition = reflection.klass.send(:sanitize_sql, interpolate(condition), reflection.table_name)
+ condition = Arel.sql(condition) unless condition.is_a?(Arel::Node)
+ condition
+ end
+
+ Arel::Nodes::And.new(conditions)
end
end
+ def polymorphic_conditions(reflection, polymorphic_reflection)
+ if polymorphic_reflection.options[:as]
+ tables[reflection][polymorphic_reflection.type].
+ eq(polymorphic_reflection.active_record.base_class.name)
+ end
+ end
+
+ def source_type_conditions(reflection)
+ if reflection.options[:source_type]
+ tables[reflection.through_reflection][reflection.foreign_type].
+ eq(reflection.options[:source_type])
+ end
+ end
+
+ # TODO: Think about this in the context of nested associations
def stale_state
if through_reflection.macro == :belongs_to
owner[through_reflection.foreign_key].to_s
@@ -153,6 +270,12 @@ module ActiveRecord
through_reflection.macro == :belongs_to &&
!owner[through_reflection.foreign_key].nil?
end
+
+ def ensure_not_nested
+ if reflection.nested?
+ raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 284ae2bebc..5833c65893 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -10,7 +10,18 @@ module ActiveRecord
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods
+ return if attribute_methods_generated?
super(column_names)
+ @attribute_methods_generated = true
+ end
+
+ def attribute_methods_generated?
+ @attribute_methods_generated ||= false
+ end
+
+ def undefine_attribute_methods(*args)
+ super
+ @attribute_methods_generated = false
end
# Checks whether the method is defined in the model or any of its subclasses
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 5de08953f9..e3e2cac042 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -265,7 +265,12 @@ module ActiveRecord
false
end
- def through_reflection_foreign_key
+ def through_reflection_chain
+ [self]
+ end
+
+ def through_conditions
+ [Array.wrap(options[:conditions])]
end
def source_reflection
@@ -363,7 +368,7 @@ 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 :association_primary_key, :foreign_type, :to => :source_reflection
+ delegate :foreign_key, :foreign_type, :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>.
@@ -392,6 +397,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]
@@ -429,10 +529,6 @@ module ActiveRecord
raise HasManyThroughAssociationPolymorphicSourceError.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
-
if macro == :has_one && through_reflection.collection?
raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection)
end
@@ -440,14 +536,6 @@ module ActiveRecord
check_validity_of_inverse!
end
- def through_reflection_primary_key
- through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.foreign_key
- end
-
- def through_reflection_foreign_key
- through_reflection.foreign_key if through_reflection.belongs_to?
- end
-
private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index cd1d7108b3..0c7a9ec56d 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -260,8 +260,9 @@ module ActiveRecord
join_list
)
+ # TODO: Necessary?
join_nodes.each do |join|
- join_dependency.table_aliases[join.left.name.downcase] = 1
+ join_dependency.alias_tracker.aliased_name_for(join.left.name.downcase)
end
join_dependency.graft(*stashed_association_joins)