From e3dab67c44ed047cc91ccfcc17c5ee2bb9997be3 Mon Sep 17 00:00:00 2001 From: Rick Olson Date: Tue, 13 Mar 2007 05:23:18 +0000 Subject: 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 --- activerecord/lib/active_record/associations.rb | 156 +++++++++++++++------ .../associations/has_many_through_association.rb | 12 +- activerecord/lib/active_record/reflection.rb | 8 +- 3 files changed, 127 insertions(+), 49 deletions(-) (limited to 'activerecord/lib/active_record') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index f0e5608b05..6fb00152a1 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -20,7 +20,13 @@ module ActiveRecord super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}'.") 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: def initialize(reflection) through_reflection = reflection.through_reflection @@ -593,6 +599,8 @@ module ActiveRecord # * :source: Specifies the source association name used by has_many :through queries. Only use it if the name cannot be # inferred from the association. has_many :subscribers, :through => :subscriptions will look for either +:subscribers+ or # +:subscriber+ on +Subscription+, unless a +:source+ is given. + # * :source_type: Specifies type of the source association used by has_many :through queries where the source association + # is a polymorphic belongs_to. # * :uniq - if set to true, duplicates will be omitted from the collection. Useful in conjunction with :through. # # Option examples: @@ -1151,7 +1159,7 @@ module ActiveRecord :class_name, :table_name, :foreign_key, :exclusively_dependent, :dependent, :select, :conditions, :include, :order, :group, :limit, :offset, - :as, :through, :source, + :as, :through, :source, :source_type, :uniq, :finder_sql, :counter_sql, :before_add, :after_add, :before_remove, :after_remove, @@ -1555,58 +1563,113 @@ module ActiveRecord case when reflection.macro == :has_many && reflection.options[:through] through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : '' + + jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil + first_key = second_key = as_extra = nil + 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) " % [ - 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 + ####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) " % [ + #### 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 - if source_reflection.macro == :has_many && source_reflection.options[:as] - " 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, - through_reflection.primary_key_name, - parent.aliased_table_name, parent.primary_key] + - " LEFT OUTER JOIN %s ON %s.%s = %s.%s AND %s.%s = %s " % [ - table_name_and_alias, - aliased_table_name, "#{source_reflection.options[:as]}_id", - aliased_join_table_name, options[:foreign_key] || primary_key, - aliased_table_name, "#{source_reflection.options[:as]}_type", - klass.quote_value(source_reflection.active_record.base_class.name) + ##if source_reflection.macro == :has_many && source_reflection.options[:as] + ####" 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, + #### through_reflection.primary_key_name, + #### parent.aliased_table_name, parent.primary_key] + + ####" LEFT OUTER JOIN %s ON %s.%s = %s.%s AND %s.%s = %s " % [ + #### table_name_and_alias, + #### aliased_table_name, "#{source_reflection.options[:as]}_id", + #### aliased_join_table_name, options[:foreign_key] || primary_key, + #### 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) ] else - case source_reflection.macro - when :belongs_to - first_key = primary_key - second_key = source_reflection.options[:foreign_key] || klass.to_s.foreign_key - extra = nil - 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 + ####case source_reflection.macro + #### when :belongs_to + #### first_key = primary_key + #### second_key = source_reflection.options[:foreign_key] || klass.to_s.foreign_key + #### extra = nil + #### 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 + second_key = options[:foreign_key] || primary_key + end + + unless through_reflection.klass.descends_from_active_record? + jt_sti_extra = " 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 + when :belongs_to + first_key = primary_key + if reflection.options[:source_type] + second_key = source_reflection.association_foreign_key + jt_source_extra = " AND %s.%s = %s" % [ + aliased_join_table_name, + reflection.active_record.connection.quote_column_name(reflection.source_reflection.options[:foreign_type]), + klass.quote_value(reflection.options[:source_type]) ] + else + second_key = source_reflection.options[:foreign_key] || klass.to_s.foreign_key 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] " LEFT OUTER JOIN %s ON %s.%s = %s.%s AND %s.%s = %s" % [ table_name_and_alias, @@ -1652,6 +1715,7 @@ module ActiveRecord end protected + def pluralize(table_name) ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name 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 26559d523a..a8ad52bef9 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -138,7 +138,11 @@ module ActiveRecord # Construct attributes for :through pointing to owner and 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 # Associate attributes pointing to owner, quoted. @@ -176,6 +180,12 @@ module ActiveRecord if @reflection.through_reflection.options[:as] || @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.table_name, "#{@reflection.source_reflection.options[:foreign_type]}", + @owner.class.quote_value(@reflection.options[:source_type]) + ] + end else reflection_primary_key = @reflection.source_reflection.primary_key_name source_primary_key = @reflection.klass.primary_key diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 2adadb29c5..62184eeff2 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -178,8 +178,12 @@ module ActiveRecord if source_reflection.nil? raise HasManyThroughSourceAssociationNotFoundError.new(self) end + + if options[:source_type] && source_reflection.options[:polymorphic].nil? + raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection) + end - if source_reflection.options[:polymorphic] + if source_reflection.options[:polymorphic] && options[:source_type].nil? raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection) end @@ -193,7 +197,7 @@ module ActiveRecord def derive_class_name # get the class_name of the belongs_to association of the through reflection if through_reflection - source_reflection.class_name + options[:source_type] || source_reflection.class_name else class_name = name.to_s.camelize class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro) -- cgit v1.2.3