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/associations.rb491
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb1
-rw-r--r--activerecord/lib/active_record/associations/nested_has_many_through.rb158
-rw-r--r--activerecord/lib/active_record/associations/through_association_scope.rb108
-rw-r--r--activerecord/lib/active_record/reflection.rb51
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb7
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb13
7 files changed, 602 insertions, 227 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 565ebf8197..67c204f154 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -111,6 +111,7 @@ module ActiveRecord
autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association'
autoload :HasManyAssociation, 'active_record/associations/has_many_association'
autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association'
+ autoload :NestedHasManyThroughAssociation, 'active_record/associations/nested_has_many_through_association'
autoload :HasOneAssociation, 'active_record/associations/has_one_association'
autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association'
@@ -1833,10 +1834,10 @@ module ActiveRecord
end
class JoinDependency # :nodoc:
- attr_reader :joins, :reflections, :table_aliases
+ attr_reader :join_parts, :reflections, :table_aliases
def initialize(base, associations, joins)
- @joins = [JoinBase.new(base, joins)]
+ @join_parts = [JoinBase.new(base, joins)]
@associations = associations
@reflections = []
@base_records_hash = {}
@@ -1849,17 +1850,17 @@ module ActiveRecord
def graft(*associations)
associations.each do |association|
join_associations.detect {|a| association == a} ||
- build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_class)
+ build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
end
self
end
def join_associations
- @joins.last(@joins.length - 1)
+ join_parts.last(join_parts.length - 1)
end
def join_base
- @joins[0]
+ join_parts.first
end
def count_aliases_from_table_joins(name)
@@ -1917,22 +1918,24 @@ module ActiveRecord
protected
- def build(associations, parent = nil, join_class = Arel::InnerJoin)
- parent ||= @joins.last
+ def build(associations, parent = nil, join_type = Arel::InnerJoin)
+ parent ||= join_parts.last
case associations
when Symbol, String
reflection = parent.reflections[associations.to_s.intern] or
raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
@reflections << reflection
- @joins << build_join_association(reflection, parent).with_join_class(join_class)
+ join_association = build_join_association(reflection, parent)
+ join_association.join_type = join_type
+ @join_parts << join_association
when Array
associations.each do |association|
- build(association, parent, join_class)
+ build(association, parent, join_type)
end
when Hash
associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
- build(name, parent, join_class)
- build(associations[name], nil, join_class)
+ build(name, parent, join_type)
+ build(associations[name], nil, join_type)
end
else
raise ConfigurationError, associations.inspect
@@ -1949,91 +1952,111 @@ module ActiveRecord
JoinAssociation.new(reflection, self, parent)
end
- def construct(parent, associations, joins, row)
+ def construct(parent, associations, join_parts, row)
case associations
when Symbol, String
- join = joins.detect{|j| j.reflection.name.to_s == associations.to_s && j.parent_table_name == parent.class.table_name }
- raise(ConfigurationError, "No such association") if join.nil?
+ join_part = join_parts.detect { |j|
+ j.reflection.name.to_s == associations.to_s &&
+ j.parent_table_name == parent.class.table_name }
+ raise(ConfigurationError, "No such association") if join_part.nil?
- joins.delete(join)
- construct_association(parent, join, row)
+ join_parts.delete(join_part)
+ construct_association(parent, join_part, row)
when Array
associations.each do |association|
- construct(parent, association, joins, row)
+ construct(parent, association, join_parts, row)
end
when Hash
associations.sort_by { |k,_| k.to_s }.each do |name, assoc|
- join = joins.detect{|j| j.reflection.name.to_s == name.to_s && j.parent_table_name == parent.class.table_name }
- raise(ConfigurationError, "No such association") if join.nil?
-
- association = construct_association(parent, join, row)
- joins.delete(join)
- construct(association, assoc, joins, row) if association
+ join_part = join_parts.detect{ |j|
+ j.reflection.name.to_s == name.to_s &&
+ j.parent_table_name == parent.class.table_name }
+ raise(ConfigurationError, "No such association") if join_part.nil?
+
+ association = construct_association(parent, join_part, row)
+ join_parts.delete(join_part)
+ construct(association, assoc, join_parts, row) if association
end
else
raise ConfigurationError, associations.inspect
end
end
- def construct_association(record, join, row)
- return if record.id.to_s != join.parent.record_id(row).to_s
+ def construct_association(record, join_part, row)
+ return if record.id.to_s != join_part.parent.record_id(row).to_s
- macro = join.reflection.macro
+ macro = join_part.reflection.macro
if macro == :has_one
- return if record.instance_variable_defined?("@#{join.reflection.name}")
- association = join.instantiate(row) unless row[join.aliased_primary_key].nil?
- set_target_and_inverse(join, association, record)
+ return if record.instance_variable_defined?("@#{join_part.reflection.name}")
+ association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
+ set_target_and_inverse(join_part, association, record)
else
- return if row[join.aliased_primary_key].nil?
- association = join.instantiate(row)
+ return if row[join_part.aliased_primary_key].nil?
+ association = join_part.instantiate(row)
case macro
when :has_many, :has_and_belongs_to_many
- collection = record.send(join.reflection.name)
+ collection = record.send(join_part.reflection.name)
collection.loaded
collection.target.push(association)
collection.__send__(:set_inverse_instance, association, record)
when :belongs_to
- set_target_and_inverse(join, association, record)
+ set_target_and_inverse(join_part, association, record)
else
- raise ConfigurationError, "unknown macro: #{join.reflection.macro}"
+ raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
end
end
association
end
- def set_target_and_inverse(join, association, record)
- association_proxy = record.send("set_#{join.reflection.name}_target", association)
+ def set_target_and_inverse(join_part, association, record)
+ association_proxy = record.send("set_#{join_part.reflection.name}_target", association)
association_proxy.__send__(:set_inverse_instance, association, record)
end
-
- class JoinBase # :nodoc:
- attr_reader :active_record, :table_joins
- delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :arel_engine, :to => :active_record
-
- def initialize(active_record, joins = nil)
+
+ # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited
+ # by JoinBase and JoinAssociation. A JoinBase represents the Active Record which
+ # everything else is being joined onto. A JoinAssociation represents an association which
+ # is joining to the base. A JoinAssociation may result in more than one actual join
+ # operations (for example a has_and_belongs_to_many JoinAssociation would result in
+ # two; one for the join table and one for the target table).
+ class JoinPart # :nodoc:
+ # The Active Record class which this join part is associated 'about'; for a JoinBase
+ # this is the actual base model, for a JoinAssociation this is the target model of the
+ # association.
+ attr_reader :active_record
+
+ delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :arel_engine, :to => :active_record
+
+ def initialize(active_record)
@active_record = active_record
@cached_record = {}
- @table_joins = joins
end
-
+
def ==(other)
- other.class == self.class &&
- other.active_record == active_record &&
- other.table_joins == table_joins
+ raise NotImplementedError
end
-
+
+ # An Arel::Table for the active_record
+ def table
+ raise NotImplementedError
+ end
+
+ # The prefix to be used when aliasing columns in the active_record's table
def aliased_prefix
- "t0"
+ raise NotImplementedError
end
-
+
+ # The alias for the active_record's table
+ def aliased_table_name
+ raise NotImplementedError
+ end
+
+ # The alias for the primary key of the active_record's table
def aliased_primary_key
"#{aliased_prefix}_r0"
end
-
- def aliased_table_name
- active_record.table_name
- end
-
+
+ # An array of [column_name, alias] pairs for the table
def column_names_with_alias
unless defined?(@column_names_with_alias)
@column_names_with_alias = []
@@ -2059,33 +2082,74 @@ module ActiveRecord
end
end
- class JoinAssociation < JoinBase # :nodoc:
- attr_reader :reflection, :parent, :aliased_table_name, :aliased_prefix, :aliased_join_table_name, :parent_table_name, :join_class
- delegate :options, :klass, :through_reflection, :source_reflection, :to => :reflection
+ class JoinBase < JoinPart # :nodoc:
+ # Extra joins provided when the JoinDependency was created
+ attr_reader :table_joins
+
+ def initialize(active_record, joins = nil)
+ super(active_record)
+ @table_joins = joins
+ end
+
+ def ==(other)
+ other.class == self.class &&
+ other.active_record == active_record &&
+ other.table_joins == table_joins
+ end
+
+ def aliased_prefix
+ "t0"
+ end
+
+ def table
+ Arel::Table.new(table_name, :engine => arel_engine, :columns => active_record.columns)
+ end
+
+ def aliased_table_name
+ active_record.table_name
+ end
+ end
+
+ class JoinAssociation < JoinPart # :nodoc:
+ # The reflection of the association represented
+ attr_reader :reflection
+
+ # The JoinDependency object which this JoinAssociation exists within. This is mainly
+ # relevant for generating aliases which do not conflict with other joins which are
+ # part of the query.
+ attr_reader :join_dependency
+
+ # A JoinBase instance representing the active record we are joining onto.
+ # (So in Author.has_many :posts, the Author would be that base record.)
+ attr_reader :parent
+
+ # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
+ attr_accessor :join_type
+
+ # These implement abstract methods from the superclass
+ attr_reader :aliased_prefix, :aliased_table_name
+
+ delegate :options, :through_reflection, :source_reflection, :to => :reflection
+ delegate :table, :table_name, :to => :parent, :prefix => true
def initialize(reflection, join_dependency, parent = nil)
reflection.check_validity!
+
if reflection.options[:polymorphic]
raise EagerLoadPolymorphicError.new(reflection)
end
super(reflection.klass)
- @join_dependency = join_dependency
- @parent = parent
- @reflection = reflection
- @aliased_prefix = "t#{ join_dependency.joins.size }"
- @parent_table_name = parent.active_record.table_name
- @aliased_table_name = aliased_table_name_for(table_name)
- @join = nil
- @join_class = Arel::InnerJoin
-
- if reflection.macro == :has_and_belongs_to_many
- @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
- end
-
- if [: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
+
+ @reflection = reflection
+ @join_dependency = join_dependency
+ @parent = parent
+ @join_type = Arel::InnerJoin
+
+ # This must be done eagerly upon initialisation because the alias which is produced
+ # depends on the state of the join dependency, but we want it to work the same way
+ # every time.
+ allocate_aliases
end
def ==(other)
@@ -2095,63 +2159,29 @@ module ActiveRecord
end
def find_parent_in(other_join_dependency)
- other_join_dependency.joins.detect do |join|
- self.parent == join
+ other_join_dependency.join_parts.detect do |join_part|
+ self.parent == join_part
end
end
-
- def with_join_class(join_class)
- @join_class = join_class
- self
- end
-
- def association_join
- return @join if @join
-
- aliased_table = Arel::Table.new(table_name, :as => @aliased_table_name,
- :engine => arel_engine,
- :columns => klass.columns)
-
- parent_table = Arel::Table.new(parent.table_name, :as => parent.aliased_table_name,
- :engine => arel_engine,
- :columns => parent.active_record.columns)
-
- @join = send("build_#{reflection.macro}", aliased_table, parent_table)
-
- unless klass.descends_from_active_record?
- sti_column = aliased_table[klass.inheritance_column]
- sti_condition = sti_column.eq(klass.sti_name)
- klass.descendants.each {|subclass| sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) }
-
- @join << sti_condition
- end
-
- [through_reflection, reflection].each do |ref|
- if ref && ref.options[:conditions]
- @join << interpolate_sql(sanitize_sql(ref.options[:conditions], aliased_table_name))
- end
- end
-
- @join
+
+ def join_to(relation)
+ send("join_#{reflection.macro}_to", relation)
end
- def relation
- aliased = Arel::Table.new(table_name, :as => @aliased_table_name,
- :engine => arel_engine,
- :columns => klass.columns)
-
- if reflection.macro == :has_and_belongs_to_many
- [Arel::Table.new(options[:join_table], :as => aliased_join_table_name, :engine => arel_engine), aliased]
- elsif reflection.options[:through]
- [Arel::Table.new(through_reflection.klass.table_name, :as => aliased_join_table_name, :engine => arel_engine), aliased]
- else
- aliased
- end
+ def join_relation(joining_relation)
+ self.join_type = Arel::OuterJoin
+ joining_relation.joins(self)
end
-
- def join_relation(joining_relation, join = nil)
- joining_relation.joins(self.with_join_class(Arel::OuterJoin))
+
+ def table
+ @table ||= Arel::Table.new(
+ table_name, :as => aliased_table_name,
+ :engine => arel_engine, :columns => active_record.columns
+ )
end
+
+ # More semantic name given we are talking about associations
+ alias_method :target_table, :table
protected
@@ -2185,7 +2215,7 @@ module ActiveRecord
end
def table_name_and_alias
- table_alias_for table_name, @aliased_table_name
+ table_alias_for table_name, aliased_table_name
end
def interpolate_sql(sql)
@@ -2193,74 +2223,169 @@ module ActiveRecord
end
private
+
+ def allocate_aliases
+ @aliased_prefix = "t#{ join_dependency.join_parts.size }"
+ @aliased_table_name = aliased_table_name_for(table_name)
+
+ if reflection.macro == :has_and_belongs_to_many
+ @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
+ elsif [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
+ @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
+ end
+ end
+
+ def process_conditions(conditions, table_name)
+ Arel.sql(interpolate_sql(sanitize_sql(conditions, table_name)))
+ end
+
+ def join_target_table(relation, *conditions)
+ relation = relation.join(target_table, join_type)
+
+ # If the target table is an STI model then we must be sure to only include records of
+ # its type and its sub-types.
+ unless active_record.descends_from_active_record?
+ sti_column = target_table[active_record.inheritance_column]
+
+ sti_condition = sti_column.eq(active_record.sti_name)
+ active_record.descendants.each do |subclass|
+ sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name))
+ end
+
+ conditions << sti_condition
+ end
+
+ # If the reflection has conditions, add them
+ if options[:conditions]
+ conditions << process_conditions(options[:conditions], aliased_table_name)
+ end
+
+ relation = relation.on(*conditions)
+ end
- def build_has_and_belongs_to_many(aliased_table, parent_table)
- join_table = Arel::Table.new(options[:join_table], :as => aliased_join_table_name, :engine => arel_engine)
- fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key
- klass_fk = options[:association_foreign_key] || klass.to_s.foreign_key
-
- [
- join_table[fk].eq(parent_table[reflection.active_record.primary_key]),
- aliased_table[klass.primary_key].eq(join_table[klass_fk])
- ]
+ def join_has_and_belongs_to_many_to(relation)
+ join_table = Arel::Table.new(
+ options[:join_table], :engine => arel_engine,
+ :as => @aliased_join_table_name
+ )
+
+ fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key
+ klass_fk = options[:association_foreign_key] || reflection.klass.to_s.foreign_key
+
+ relation = relation.join(join_table, join_type)
+ relation = relation.on(
+ join_table[fk].
+ eq(parent_table[reflection.active_record.primary_key])
+ )
+
+ join_target_table(
+ relation,
+ target_table[reflection.klass.primary_key].
+ eq(join_table[klass_fk])
+ )
end
- def build_has_many(aliased_table, parent_table)
+ def join_has_many_to(relation)
if reflection.options[:through]
- join_table = Arel::Table.new(through_reflection.klass.table_name,
- :as => aliased_join_table_name,
- :engine => arel_engine)
- jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil
- 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_as_extra = 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
+ join_has_many_through_to(relation)
+ elsif reflection.options[:as]
+ join_has_many_polymorphic_to(relation)
+ else
+ foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
+ primary_key = options[:primary_key] || parent.primary_key
+
+ join_target_table(
+ relation,
+ target_table[foreign_key].
+ eq(parent_table[primary_key])
+ )
+ end
+ end
+ alias :join_has_one_to :join_has_many_to
+
+ def join_has_many_through_to(relation)
+ join_table = Arel::Table.new(
+ through_reflection.klass.table_name, :engine => arel_engine,
+ :as => @aliased_join_table_name
+ )
+
+ jt_conditions = []
+ jt_foreign_key = first_key = second_key = nil
+
+ if through_reflection.options[:as] # has_many :through against a polymorphic join
+ as_key = through_reflection.options[:as].to_s
+ jt_foreign_key = as_key + '_id'
+
+ jt_conditions <<
+ join_table[as_key + '_type'].
+ eq(parent.active_record.base_class.name)
+ else
+ jt_foreign_key = through_reflection.primary_key_name
+ end
- 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
+ case source_reflection.macro
+ when :has_many
+ second_key = options[:foreign_key] || primary_key
- unless through_reflection.klass.descends_from_active_record?
- jt_sti_extra = 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_source_extra = join_table[reflection.source_reflection.options[:foreign_type]].eq(reflection.options[:source_type])
- else
- second_key = source_reflection.primary_key_name
- end
+ 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
- [
- [parent_table[parent.primary_key].eq(join_table[jt_foreign_key]), jt_as_extra, jt_source_extra, jt_sti_extra].compact,
- aliased_table[first_key].eq(join_table[second_key])
- ]
- elsif reflection.options[:as]
- id_rel = aliased_table["#{reflection.options[:as]}_id"].eq(parent_table[parent.primary_key])
- type_rel = aliased_table["#{reflection.options[:as]}_type"].eq(parent.active_record.base_class.name)
- [id_rel, type_rel]
- else
- foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
- [aliased_table[foreign_key].eq(parent_table[reflection.options[:primary_key] || parent.primary_key])]
+ unless through_reflection.klass.descends_from_active_record?
+ jt_conditions <<
+ join_table[through_reflection.active_record.inheritance_column].
+ eq(through_reflection.klass.sti_name)
+ end
+ when :belongs_to
+ first_key = primary_key
+
+ if reflection.options[:source_type]
+ second_key = source_reflection.association_foreign_key
+
+ jt_conditions <<
+ join_table[reflection.source_reflection.options[:foreign_type]].
+ eq(reflection.options[:source_type])
+ else
+ second_key = source_reflection.primary_key_name
+ end
+ end
+
+ jt_conditions <<
+ parent_table[parent.primary_key].
+ eq(join_table[jt_foreign_key])
+
+ if through_reflection.options[:conditions]
+ jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name)
end
+
+ relation = relation.join(join_table, join_type).on(*jt_conditions)
+
+ join_target_table(
+ relation,
+ target_table[first_key].eq(join_table[second_key])
+ )
+ end
+
+ def join_has_many_polymorphic_to(relation)
+ join_target_table(
+ relation,
+ target_table["#{reflection.options[:as]}_id"].
+ eq(parent_table[parent.primary_key]),
+ target_table["#{reflection.options[:as]}_type"].
+ eq(parent.active_record.base_class.name)
+ )
end
- alias :build_has_one :build_has_many
- def build_belongs_to(aliased_table, parent_table)
- [aliased_table[options[:primary_key] || reflection.klass.primary_key].eq(parent_table[options[:foreign_key] || reflection.primary_key_name])]
+ def join_belongs_to_to(relation)
+ foreign_key = options[:foreign_key] || reflection.primary_key_name
+ primary_key = options[:primary_key] || reflection.klass.primary_key
+
+ join_target_table(
+ relation,
+ target_table[primary_key].eq(parent_table[foreign_key])
+ )
end
end
end
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 97883d8393..313d9da621 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -1,4 +1,5 @@
require "active_record/associations/through_association_scope"
+require "active_record/associations/nested_has_many_through"
require 'active_support/core_ext/object/blank'
module ActiveRecord
diff --git a/activerecord/lib/active_record/associations/nested_has_many_through.rb b/activerecord/lib/active_record/associations/nested_has_many_through.rb
new file mode 100644
index 0000000000..d699a60edb
--- /dev/null
+++ b/activerecord/lib/active_record/associations/nested_has_many_through.rb
@@ -0,0 +1,158 @@
+# TODO: Remove in the end, when its functionality is fully integrated in ThroughAssociationScope.
+
+module ActiveRecord
+ module Associations
+ module NestedHasManyThrough
+ def self.included(klass)
+ klass.alias_method_chain :construct_conditions, :nesting
+ klass.alias_method_chain :construct_joins, :nesting
+ end
+
+ def construct_joins_with_nesting(custom_joins = nil)
+ if nested?
+ @nested_join_attributes ||= construct_nested_join_attributes
+ "#{construct_nested_join_attributes[:joins]} #{@reflection.options[:joins]} #{custom_joins}"
+ else
+ construct_joins_without_nesting(custom_joins)
+ end
+ end
+
+ def construct_conditions_with_nesting
+ if nested?
+ @nested_join_attributes ||= construct_nested_join_attributes
+ if @reflection.through_reflection && @reflection.through_reflection.macro == :belongs_to
+ "#{@nested_join_attributes[:remote_key]} = #{belongs_to_quoted_key} #{@nested_join_attributes[:conditions]}"
+ else
+ "#{@nested_join_attributes[:remote_key]} = #{@owner.quoted_id} #{@nested_join_attributes[:conditions]}"
+ end
+ else
+ construct_conditions_without_nesting
+ end
+ end
+
+ protected
+
+ # Given any belongs_to or has_many (including has_many :through) association,
+ # return the essential components of a join corresponding to that association, namely:
+ #
+ # * <tt>:joins</tt>: any additional joins required to get from the association's table
+ # (reflection.table_name) to the table that's actually joining to the active record's table
+ # * <tt>:remote_key</tt>: the name of the key in the join table (qualified by table name) which will join
+ # to a field of the active record's table
+ # * <tt>:local_key</tt>: the name of the key in the local table (not qualified by table name) which will
+ # take part in the join
+ # * <tt>:conditions</tt>: any additional conditions (e.g. filtering by type for a polymorphic association,
+ # or a :conditions clause explicitly given in the association), including a leading AND
+ def construct_nested_join_attributes(reflection = @reflection, association_class = reflection.klass, table_ids = {association_class.table_name => 1})
+ if (reflection.macro == :has_many || reflection.macro == :has_one) && reflection.through_reflection
+ construct_has_many_through_attributes(reflection, table_ids)
+ else
+ construct_has_many_or_belongs_to_attributes(reflection, association_class, table_ids)
+ end
+ end
+
+ def construct_has_many_through_attributes(reflection, table_ids)
+ # Construct the join components of the source association, so that we have a path from
+ # the eventual target table of the association up to the table named in :through, and
+ # all tables involved are allocated table IDs.
+ source_attrs = construct_nested_join_attributes(reflection.source_reflection, reflection.klass, table_ids)
+
+ # Determine the alias of the :through table; this will be the last table assigned
+ # when constructing the source join components above.
+ through_table_alias = through_table_name = reflection.through_reflection.table_name
+ through_table_alias += "_#{table_ids[through_table_name]}" unless table_ids[through_table_name] == 1
+
+ # Construct the join components of the through association, so that we have a path to
+ # the active record's table.
+ through_attrs = construct_nested_join_attributes(reflection.through_reflection, reflection.through_reflection.klass, table_ids)
+
+ # Any subsequent joins / filters on owner attributes will act on the through association,
+ # so that's what we return for the conditions/keys of the overall association.
+ conditions = through_attrs[:conditions]
+ conditions += " AND #{interpolate_sql(reflection.klass.send(:sanitize_sql, reflection.options[:conditions]))}" if reflection.options[:conditions]
+
+ {
+ :joins => "%s INNER JOIN %s ON ( %s = %s.%s %s) %s %s" % [
+ source_attrs[:joins],
+ through_table_name == through_table_alias ? through_table_name : "#{through_table_name} #{through_table_alias}",
+ source_attrs[:remote_key],
+ through_table_alias, source_attrs[:local_key],
+ source_attrs[:conditions],
+ through_attrs[:joins],
+ reflection.options[:joins]
+ ],
+ :remote_key => through_attrs[:remote_key],
+ :local_key => through_attrs[:local_key],
+ :conditions => conditions
+ }
+ end
+
+ # reflection is not has_many :through; it's a standard has_many / belongs_to instead
+ # TODO: see if we can defer to rails code here a bit more
+ def construct_has_many_or_belongs_to_attributes(reflection, association_class, table_ids)
+ # Determine the alias used for remote_table_name, if any. In all cases this will already
+ # have been assigned an ID in table_ids (either through being involved in a previous join,
+ # or - if it's the first table in the query - as the default value of table_ids)
+ remote_table_alias = remote_table_name = association_class.table_name
+ remote_table_alias += "_#{table_ids[remote_table_name]}" unless table_ids[remote_table_name] == 1
+
+ # Assign a new alias for the local table.
+ local_table_alias = local_table_name = reflection.active_record.table_name
+ if table_ids[local_table_name]
+ table_id = table_ids[local_table_name] += 1
+ local_table_alias += "_#{table_id}"
+ else
+ table_ids[local_table_name] = 1
+ end
+
+ conditions = ''
+ # Add type_condition, if applicable
+ conditions += " AND #{association_class.send(:type_condition).to_sql}" if association_class.finder_needs_type_condition?
+ # Add custom conditions
+ conditions += " AND (#{interpolate_sql(association_class.send(:sanitize_sql, reflection.options[:conditions]))})" if reflection.options[:conditions]
+
+ if reflection.macro == :belongs_to
+ if reflection.options[:polymorphic]
+ conditions += " AND #{local_table_alias}.#{reflection.options[:foreign_type]} = #{reflection.active_record.quote_value(association_class.base_class.name.to_s)}"
+ end
+ {
+ :joins => reflection.options[:joins],
+ :remote_key => "#{remote_table_alias}.#{association_class.primary_key}",
+ :local_key => reflection.primary_key_name,
+ :conditions => conditions
+ }
+ else
+ # Association is has_many (without :through)
+ if reflection.options[:as]
+ conditions += " AND #{remote_table_alias}.#{reflection.options[:as]}_type = #{reflection.active_record.quote_value(reflection.active_record.base_class.name.to_s)}"
+ end
+ {
+ :joins => "#{reflection.options[:joins]}",
+ :remote_key => "#{remote_table_alias}.#{reflection.primary_key_name}",
+ :local_key => reflection.klass.primary_key,
+ :conditions => conditions
+ }
+ end
+ end
+
+ def belongs_to_quoted_key
+ attribute = @reflection.through_reflection.primary_key_name
+ column = @owner.column_for_attribute attribute
+
+ @owner.send(:quote_value, @owner.send(attribute), column)
+ end
+
+ def nested?
+ through_source_reflection? || through_through_reflection?
+ end
+
+ def through_source_reflection?
+ @reflection.source_reflection && @reflection.source_reflection.options[:through]
+ end
+
+ def through_through_reflection?
+ @reflection.through_reflection && @reflection.through_reflection.options[:through]
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb
index cabb33c4a8..90ebadda89 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
@@ -19,9 +21,9 @@ module ActiveRecord
# 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
+ conditions = construct_quoted_owner_attributes(reflection).map do |attr, value|
+ "#{table_aliases[reflection]}.#{attr} = #{value}"
end
conditions << sql_conditions if sql_conditions
"(" + conditions.join(') AND (') + ")"
@@ -49,35 +51,87 @@ module ActiveRecord
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])
- ]
+ # puts @reflection.through_reflection_chain.map(&:inspect)
+
+ "#{construct_through_joins} #{@reflection.options[:joins]} #{custom_joins}"
+ end
+
+ def construct_through_joins
+ joins = []
+
+ # Iterate over each pair in the through reflection chain, joining them together
+ @reflection.through_reflection_chain.each_cons(2) do |left, right|
+ polymorphic_join = nil
+
+ case
+ when left.source_reflection.nil?
+ left_primary_key = left.primary_key_name
+ right_primary_key = right.klass.primary_key
+
+ if left.options[:as]
+ polymorphic_join = "AND %s.%s = %s" % [
+ table_aliases[left], "#{left.options[:as]}_type",
+ @owner.class.quote_value(right.klass.name)
+ ]
+ end
+ when left.source_reflection.macro == :belongs_to
+ left_primary_key = left.klass.primary_key
+ right_primary_key = left.source_reflection.primary_key_name
+
+ if left.options[:source_type]
+ polymorphic_join = "AND %s.%s = %s" % [
+ table_aliases[right],
+ left.source_reflection.options[:foreign_type].to_s,
+ @owner.class.quote_value(left.options[:source_type])
+ ]
+ end
+ else
+ left_primary_key = left.source_reflection.primary_key_name
+ right_primary_key = right.klass.primary_key
+
+ if left.source_reflection.options[:as]
+ polymorphic_join = "AND %s.%s = %s" % [
+ table_aliases[left],
+ "#{left.source_reflection.options[:as]}_type",
+ @owner.class.quote_value(right.klass.name)
+ ]
+ 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)
- ]
+
+ if right.quoted_table_name == table_aliases[right]
+ table = right.quoted_table_name
+ else
+ table = "#{right.quoted_table_name} #{table_aliases[right]}"
end
+
+ joins << "INNER JOIN %s ON %s.%s = %s.%s %s" % [
+ table,
+ table_aliases[left], left_primary_key,
+ table_aliases[right], right_primary_key,
+ polymorphic_join
+ ]
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 table_aliases
+ @table_aliases ||= begin
+ tally = {}
+ @reflection.through_reflection_chain.inject({}) do |aliases, reflection|
+ if tally[reflection.table_name].nil?
+ tally[reflection.table_name] = 1
+ aliases[reflection] = reflection.quoted_table_name
+ else
+ tally[reflection.table_name] += 1
+ aliased_table_name = reflection.table_name + "_#{tally[reflection.table_name]}"
+ aliases[reflection] = reflection.klass.connection.quote_table_name(aliased_table_name)
+ end
+ aliases
+ end
+ end
end
# Construct attributes for associate pointing to owner.
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index db18fb7c0f..b7cd466e13 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -131,6 +131,14 @@ module ActiveRecord
@sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
end
+ # TODO: Remove these in the final patch. I am just using them for debugging etc.
+ def inspect
+ "#<#{code_name}>"
+ end
+ def code_name
+ "#{active_record.name}.#{macro} :#{name}"
+ end
+
private
def derive_class_name
name.to_s.camelize
@@ -241,6 +249,10 @@ module ActiveRecord
def through_reflection
false
end
+
+ def through_reflection_chain
+ [self]
+ end
def through_reflection_primary_key_name
end
@@ -304,6 +316,16 @@ module ActiveRecord
def belongs_to?
macro == :belongs_to
end
+
+ # TODO: Remove for final patch. Just here for debugging.
+ def inspect
+ str = "#<#{code_name}, @source_reflection="
+ str << (source_reflection.respond_to?(:code_name) ? source_reflection.code_name : source_reflection.inspect)
+ str << ", @through_reflection="
+ str << (through_reflection.respond_to?(:code_name) ? through_reflection.code_name : through_reflection.inspect)
+ str << ">"
+ str
+ end
private
def derive_class_name
@@ -352,6 +374,27 @@ module ActiveRecord
def through_reflection
@through_reflection ||= active_record.reflect_on_association(options[:through])
end
+
+ # TODO: Documentation
+ def through_reflection_chain
+ @through_reflection_chain ||= begin
+ if source_reflection.through_reflection
+ # If the source reflection goes through another reflection, then the chain must start
+ # by getting us to the 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
# Gets an array of possible <tt>:through</tt> source reflection names:
#
@@ -378,9 +421,11 @@ 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
+ # TODO: Presumably remove the HasManyThroughSourceAssociationMacroError class and delete these lines.
+ # Think about whether there are any cases which should still be disallowed.
+ # 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
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index ede1c8821e..5034caf084 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -343,8 +343,11 @@ module ActiveRecord
end
def column_aliases(join_dependency)
- join_dependency.joins.collect{|join| join.column_names_with_alias.collect{|column_name, aliased_name|
- "#{connection.quote_table_name join.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}"}}.flatten.join(", ")
+ join_dependency.join_parts.collect { |join_part|
+ join_part.column_names_with_alias.collect{ |column_name, aliased_name|
+ "#{connection.quote_table_name join_part.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}"
+ }
+ }.flatten.join(", ")
end
def using_limitable_reflections?(reflections)
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 001207514d..f314ff861f 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -230,19 +230,8 @@ module ActiveRecord
@implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
- to_join = []
-
join_dependency.join_associations.each do |association|
- if (association_relation = association.relation).is_a?(Array)
- to_join << [association_relation.first, association.join_class, association.association_join.first]
- to_join << [association_relation.last, association.join_class, association.association_join.last]
- else
- to_join << [association_relation, association.join_class, association.association_join]
- end
- end
-
- to_join.uniq.each do |left, join_class, right|
- relation = relation.join(left, join_class).on(*right)
+ relation = association.join_to(relation)
end
relation.join(custom_joins)