mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Allow a polymorphic :source for has_many :through associations. Closes #7143 [protocool]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@6408 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
2823a56f14
commit
e3dab67c44
6 changed files with 143 additions and 49 deletions
|
@ -1,5 +1,7 @@
|
||||||
*SVN*
|
*SVN*
|
||||||
|
|
||||||
|
* Allow a polymorphic :source for has_many :through associations. Closes #7143 [protocool]
|
||||||
|
|
||||||
* Consistent public/protected/private visibility for chained methods. #7813 [Dan Manges]
|
* Consistent public/protected/private visibility for chained methods. #7813 [Dan Manges]
|
||||||
|
|
||||||
* Oracle: fix quoted primary keys and datetime overflow. #7798 [Michael Schoen]
|
* Oracle: fix quoted primary keys and datetime overflow. #7798 [Michael Schoen]
|
||||||
|
|
|
@ -21,6 +21,12 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc:
|
||||||
|
def initialize(owner_class_name, reflection, source_reflection)
|
||||||
|
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc:
|
class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc:
|
||||||
def initialize(reflection)
|
def initialize(reflection)
|
||||||
through_reflection = reflection.through_reflection
|
through_reflection = reflection.through_reflection
|
||||||
|
@ -593,6 +599,8 @@ module ActiveRecord
|
||||||
# * <tt>:source</tt>: Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be
|
# * <tt>:source</tt>: Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be
|
||||||
# inferred from the association. <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either +:subscribers+ or
|
# inferred from the association. <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either +:subscribers+ or
|
||||||
# +:subscriber+ on +Subscription+, unless a +:source+ is given.
|
# +:subscriber+ on +Subscription+, unless a +:source+ is given.
|
||||||
|
# * <tt>:source_type</tt>: Specifies type of the source association used by <tt>has_many :through</tt> queries where the source association
|
||||||
|
# is a polymorphic belongs_to.
|
||||||
# * <tt>:uniq</tt> - if set to true, duplicates will be omitted from the collection. Useful in conjunction with :through.
|
# * <tt>:uniq</tt> - if set to true, duplicates will be omitted from the collection. Useful in conjunction with :through.
|
||||||
#
|
#
|
||||||
# Option examples:
|
# Option examples:
|
||||||
|
@ -1151,7 +1159,7 @@ module ActiveRecord
|
||||||
:class_name, :table_name, :foreign_key,
|
:class_name, :table_name, :foreign_key,
|
||||||
:exclusively_dependent, :dependent,
|
:exclusively_dependent, :dependent,
|
||||||
:select, :conditions, :include, :order, :group, :limit, :offset,
|
:select, :conditions, :include, :order, :group, :limit, :offset,
|
||||||
:as, :through, :source,
|
:as, :through, :source, :source_type,
|
||||||
:uniq,
|
:uniq,
|
||||||
:finder_sql, :counter_sql,
|
:finder_sql, :counter_sql,
|
||||||
:before_add, :after_add, :before_remove, :after_remove,
|
:before_add, :after_add, :before_remove, :after_remove,
|
||||||
|
@ -1555,58 +1563,113 @@ module ActiveRecord
|
||||||
case
|
case
|
||||||
when reflection.macro == :has_many && reflection.options[:through]
|
when reflection.macro == :has_many && reflection.options[:through]
|
||||||
through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : ''
|
through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : ''
|
||||||
if through_reflection.options[:as] # has_many :through against a polymorphic join
|
|
||||||
polymorphic_foreign_key = through_reflection.options[:as].to_s + '_id'
|
|
||||||
polymorphic_foreign_type = through_reflection.options[:as].to_s + '_type'
|
|
||||||
|
|
||||||
" LEFT OUTER JOIN %s ON (%s.%s = %s.%s AND %s.%s = %s) " % [
|
jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil
|
||||||
table_alias_for(through_reflection.klass.table_name, aliased_join_table_name),
|
first_key = second_key = as_extra = nil
|
||||||
aliased_join_table_name, polymorphic_foreign_key,
|
|
||||||
parent.aliased_table_name, parent.primary_key,
|
if through_reflection.options[:as] # has_many :through against a polymorphic join
|
||||||
aliased_join_table_name, polymorphic_foreign_type, klass.quote_value(parent.active_record.base_class.name)] +
|
####polymorphic_foreign_key = through_reflection.options[:as].to_s + '_id'
|
||||||
" LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [table_name_and_alias,
|
####polymorphic_foreign_type = through_reflection.options[:as].to_s + '_type'
|
||||||
aliased_table_name, primary_key, aliased_join_table_name, options[:foreign_key] || reflection.klass.to_s.foreign_key
|
####
|
||||||
|
####" LEFT OUTER JOIN %s ON (%s.%s = %s.%s AND %s.%s = %s) " % [
|
||||||
|
#### table_alias_for(through_reflection.klass.table_name, aliased_join_table_name),
|
||||||
|
#### aliased_join_table_name, polymorphic_foreign_key,
|
||||||
|
#### parent.aliased_table_name, parent.primary_key,
|
||||||
|
#### aliased_join_table_name, polymorphic_foreign_type, klass.quote_value(parent.active_record.base_class.name)] +
|
||||||
|
####" LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [table_name_and_alias,
|
||||||
|
#### aliased_table_name, primary_key, aliased_join_table_name, options[:foreign_key] || reflection.klass.to_s.foreign_key
|
||||||
|
jt_foreign_key = through_reflection.options[:as].to_s + '_id'
|
||||||
|
jt_as_extra = " AND %s.%s = %s" % [
|
||||||
|
aliased_join_table_name,
|
||||||
|
reflection.active_record.connection.quote_column_name(through_reflection.options[:as].to_s + '_type'),
|
||||||
|
klass.quote_value(parent.active_record.base_class.name)
|
||||||
]
|
]
|
||||||
else
|
else
|
||||||
if source_reflection.macro == :has_many && source_reflection.options[:as]
|
##if source_reflection.macro == :has_many && source_reflection.options[:as]
|
||||||
" LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [
|
####" LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [
|
||||||
table_alias_for(through_reflection.klass.table_name, aliased_join_table_name), aliased_join_table_name,
|
#### table_alias_for(through_reflection.klass.table_name, aliased_join_table_name), aliased_join_table_name,
|
||||||
through_reflection.primary_key_name,
|
#### through_reflection.primary_key_name,
|
||||||
parent.aliased_table_name, parent.primary_key] +
|
#### parent.aliased_table_name, parent.primary_key] +
|
||||||
" LEFT OUTER JOIN %s ON %s.%s = %s.%s AND %s.%s = %s " % [
|
####" LEFT OUTER JOIN %s ON %s.%s = %s.%s AND %s.%s = %s " % [
|
||||||
table_name_and_alias,
|
#### table_name_and_alias,
|
||||||
aliased_table_name, "#{source_reflection.options[:as]}_id",
|
#### aliased_table_name, "#{source_reflection.options[:as]}_id",
|
||||||
aliased_join_table_name, options[:foreign_key] || primary_key,
|
#### aliased_join_table_name, options[:foreign_key] || primary_key,
|
||||||
aliased_table_name, "#{source_reflection.options[:as]}_type",
|
#### aliased_table_name, "#{source_reflection.options[:as]}_type",
|
||||||
|
#### klass.quote_value(source_reflection.active_record.base_class.name)
|
||||||
|
jt_foreign_key = through_reflection.primary_key_name
|
||||||
|
end
|
||||||
|
|
||||||
|
case source_reflection.macro
|
||||||
|
when :has_many
|
||||||
|
if source_reflection.options[:as]
|
||||||
|
first_key = "#{source_reflection.options[:as]}_id"
|
||||||
|
second_key = options[:foreign_key] || primary_key
|
||||||
|
as_extra = " AND %s.%s = %s" % [
|
||||||
|
aliased_table_name,
|
||||||
|
reflection.active_record.connection.quote_column_name("#{source_reflection.options[:as]}_type"),
|
||||||
klass.quote_value(source_reflection.active_record.base_class.name)
|
klass.quote_value(source_reflection.active_record.base_class.name)
|
||||||
]
|
]
|
||||||
else
|
else
|
||||||
case source_reflection.macro
|
####case source_reflection.macro
|
||||||
when :belongs_to
|
#### when :belongs_to
|
||||||
first_key = primary_key
|
#### first_key = primary_key
|
||||||
second_key = source_reflection.options[:foreign_key] || klass.to_s.foreign_key
|
#### second_key = source_reflection.options[:foreign_key] || klass.to_s.foreign_key
|
||||||
extra = nil
|
#### extra = nil
|
||||||
when :has_many
|
#### when :has_many
|
||||||
|
#### first_key = through_reflection.klass.base_class.to_s.foreign_key
|
||||||
|
#### second_key = options[:foreign_key] || primary_key
|
||||||
|
#### extra = through_reflection.klass.descends_from_active_record? ? nil :
|
||||||
|
#### " AND %s.%s = %s" % [
|
||||||
|
#### aliased_join_table_name,
|
||||||
|
#### reflection.active_record.connection.quote_column_name(through_reflection.active_record.inheritance_column),
|
||||||
|
#### through_reflection.klass.quote_value(through_reflection.klass.name.demodulize)]
|
||||||
|
####end
|
||||||
|
####" LEFT OUTER JOIN %s ON (%s.%s = %s.%s%s) " % [
|
||||||
|
#### table_alias_for(through_reflection.klass.table_name, aliased_join_table_name),
|
||||||
|
#### aliased_join_table_name, through_reflection.primary_key_name,
|
||||||
|
#### parent.aliased_table_name, parent.primary_key, extra] +
|
||||||
|
####" LEFT OUTER JOIN %s ON (%s.%s = %s.%s) " % [
|
||||||
|
#### table_name_and_alias,
|
||||||
|
#### aliased_table_name, first_key,
|
||||||
|
#### aliased_join_table_name, second_key
|
||||||
|
####]
|
||||||
first_key = through_reflection.klass.base_class.to_s.foreign_key
|
first_key = through_reflection.klass.base_class.to_s.foreign_key
|
||||||
second_key = options[:foreign_key] || primary_key
|
second_key = options[:foreign_key] || primary_key
|
||||||
extra = through_reflection.klass.descends_from_active_record? ? nil :
|
end
|
||||||
" AND %s.%s = %s" % [
|
|
||||||
|
unless through_reflection.klass.descends_from_active_record?
|
||||||
|
jt_sti_extra = " AND %s.%s = %s" % [
|
||||||
aliased_join_table_name,
|
aliased_join_table_name,
|
||||||
reflection.active_record.connection.quote_column_name(through_reflection.active_record.inheritance_column),
|
reflection.active_record.connection.quote_column_name(through_reflection.active_record.inheritance_column),
|
||||||
through_reflection.klass.quote_value(through_reflection.klass.name.demodulize)]
|
through_reflection.klass.quote_value(through_reflection.klass.name.demodulize)]
|
||||||
end
|
end
|
||||||
" LEFT OUTER JOIN %s ON (%s.%s = %s.%s%s) " % [
|
when :belongs_to
|
||||||
table_alias_for(through_reflection.klass.table_name, aliased_join_table_name),
|
first_key = primary_key
|
||||||
aliased_join_table_name, through_reflection.primary_key_name,
|
if reflection.options[:source_type]
|
||||||
parent.aliased_table_name, parent.primary_key, extra] +
|
second_key = source_reflection.association_foreign_key
|
||||||
" LEFT OUTER JOIN %s ON (%s.%s = %s.%s) " % [
|
jt_source_extra = " AND %s.%s = %s" % [
|
||||||
table_name_and_alias,
|
aliased_join_table_name,
|
||||||
aliased_table_name, first_key,
|
reflection.active_record.connection.quote_column_name(reflection.source_reflection.options[:foreign_type]),
|
||||||
aliased_join_table_name, second_key
|
klass.quote_value(reflection.options[:source_type])
|
||||||
]
|
]
|
||||||
|
else
|
||||||
|
second_key = source_reflection.options[:foreign_key] || klass.to_s.foreign_key
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
" LEFT OUTER JOIN %s ON (%s.%s = %s.%s%s%s%s) " % [
|
||||||
|
table_alias_for(through_reflection.klass.table_name, aliased_join_table_name),
|
||||||
|
parent.aliased_table_name, reflection.active_record.connection.quote_column_name(parent.primary_key),
|
||||||
|
aliased_join_table_name, reflection.active_record.connection.quote_column_name(jt_foreign_key),
|
||||||
|
jt_as_extra, jt_source_extra, jt_sti_extra
|
||||||
|
] +
|
||||||
|
" LEFT OUTER JOIN %s ON (%s.%s = %s.%s%s) " % [
|
||||||
|
table_name_and_alias,
|
||||||
|
aliased_table_name, reflection.active_record.connection.quote_column_name(first_key),
|
||||||
|
aliased_join_table_name, reflection.active_record.connection.quote_column_name(second_key),
|
||||||
|
as_extra
|
||||||
|
]
|
||||||
|
|
||||||
when reflection.macro == :has_many && reflection.options[:as]
|
when reflection.macro == :has_many && reflection.options[:as]
|
||||||
" LEFT OUTER JOIN %s ON %s.%s = %s.%s AND %s.%s = %s" % [
|
" LEFT OUTER JOIN %s ON %s.%s = %s.%s AND %s.%s = %s" % [
|
||||||
table_name_and_alias,
|
table_name_and_alias,
|
||||||
|
@ -1652,6 +1715,7 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def pluralize(table_name)
|
def pluralize(table_name)
|
||||||
ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
|
ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
|
||||||
end
|
end
|
||||||
|
|
|
@ -138,7 +138,11 @@ module ActiveRecord
|
||||||
|
|
||||||
# Construct attributes for :through pointing to owner and associate.
|
# Construct attributes for :through pointing to owner and associate.
|
||||||
def construct_join_attributes(associate)
|
def construct_join_attributes(associate)
|
||||||
construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id)
|
returning construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id) do |join_attributes|
|
||||||
|
if @reflection.options[:source_type]
|
||||||
|
join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Associate attributes pointing to owner, quoted.
|
# Associate attributes pointing to owner, quoted.
|
||||||
|
@ -176,6 +180,12 @@ module ActiveRecord
|
||||||
if @reflection.through_reflection.options[:as] || @reflection.source_reflection.macro == :belongs_to
|
if @reflection.through_reflection.options[:as] || @reflection.source_reflection.macro == :belongs_to
|
||||||
reflection_primary_key = @reflection.klass.primary_key
|
reflection_primary_key = @reflection.klass.primary_key
|
||||||
source_primary_key = @reflection.source_reflection.primary_key_name
|
source_primary_key = @reflection.source_reflection.primary_key_name
|
||||||
|
if @reflection.options[:source_type]
|
||||||
|
polymorphic_join = "AND %s.%s = %s" % [
|
||||||
|
@reflection.through_reflection.table_name, "#{@reflection.source_reflection.options[:foreign_type]}",
|
||||||
|
@owner.class.quote_value(@reflection.options[:source_type])
|
||||||
|
]
|
||||||
|
end
|
||||||
else
|
else
|
||||||
reflection_primary_key = @reflection.source_reflection.primary_key_name
|
reflection_primary_key = @reflection.source_reflection.primary_key_name
|
||||||
source_primary_key = @reflection.klass.primary_key
|
source_primary_key = @reflection.klass.primary_key
|
||||||
|
|
|
@ -179,7 +179,11 @@ module ActiveRecord
|
||||||
raise HasManyThroughSourceAssociationNotFoundError.new(self)
|
raise HasManyThroughSourceAssociationNotFoundError.new(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
if source_reflection.options[:polymorphic]
|
if options[:source_type] && source_reflection.options[:polymorphic].nil?
|
||||||
|
raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
|
||||||
|
end
|
||||||
|
|
||||||
|
if source_reflection.options[:polymorphic] && options[:source_type].nil?
|
||||||
raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection)
|
raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -193,7 +197,7 @@ module ActiveRecord
|
||||||
def derive_class_name
|
def derive_class_name
|
||||||
# get the class_name of the belongs_to association of the through reflection
|
# get the class_name of the belongs_to association of the through reflection
|
||||||
if through_reflection
|
if through_reflection
|
||||||
source_reflection.class_name
|
options[:source_type] || source_reflection.class_name
|
||||||
else
|
else
|
||||||
class_name = name.to_s.camelize
|
class_name = name.to_s.camelize
|
||||||
class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro)
|
class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro)
|
||||||
|
|
|
@ -301,6 +301,18 @@ class AssociationsJoinModelTest < Test::Unit::TestCase
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_has_many_polymorphic_with_source_type
|
||||||
|
assert_equal [posts(:welcome), posts(:thinking)], tags(:general).tagged_posts
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_eager_has_many_polymorphic_with_source_type
|
||||||
|
tag_with_include = Tag.find(tags(:general).id, :include => :tagged_posts)
|
||||||
|
desired = [posts(:welcome), posts(:thinking)]
|
||||||
|
assert_no_queries do
|
||||||
|
assert_equal desired, tag_with_include.tagged_posts
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def test_has_many_through_has_many_find_all
|
def test_has_many_through_has_many_find_all
|
||||||
assert_equal comments(:greetings), authors(:david).comments.find(:all, :order => 'comments.id').first
|
assert_equal comments(:greetings), authors(:david).comments.find(:all, :order => 'comments.id').first
|
||||||
end
|
end
|
||||||
|
|
2
activerecord/test/fixtures/tag.rb
vendored
2
activerecord/test/fixtures/tag.rb
vendored
|
@ -2,4 +2,6 @@ class Tag < ActiveRecord::Base
|
||||||
has_many :taggings
|
has_many :taggings
|
||||||
has_many :taggables, :through => :taggings
|
has_many :taggables, :through => :taggings
|
||||||
has_one :tagging
|
has_one :tagging
|
||||||
|
|
||||||
|
has_many :tagged_posts, :through => :taggings, :source => :taggable, :source_type => 'Post'
|
||||||
end
|
end
|
Loading…
Reference in a new issue