diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 70bc4a4d651c189ee34abd084350f4e29d2fe76b..86b7101c6430429385c771f54d263e70572bcb65 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1,6 +1,7 @@ require 'active_record/associations/association_proxy' require 'active_record/associations/association_collection' require 'active_record/associations/belongs_to_association' +require 'active_record/associations/belongs_to_polymorphic_association' require 'active_record/associations/has_one_association' require 'active_record/associations/has_many_association' require 'active_record/associations/has_and_belongs_to_many_association' @@ -344,7 +345,7 @@ def has_many(association_id, options = {}, &extension) :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :include, :finder_sql, :counter_sql, :before_add, :after_add, :before_remove, :after_remove, :extend, - :group + :group, :as ) options[:extend] = create_extension_module(association_id, extension) if block_given? @@ -516,47 +517,73 @@ def has_one(association_id, options = {}) # belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id", # :conditions => 'discounts > #{payments_count}' def belongs_to(association_id, options = {}) - options.assert_valid_keys(:class_name, :foreign_key, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend) + options.assert_valid_keys(:class_name, :foreign_key, :foreign_type, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend, :polymorphic) association_name, association_class_name, class_primary_key_name = associate_identification(association_id, options[:class_name], options[:foreign_key], false) - require_association_class(association_class_name) - association_class_primary_key_name = options[:foreign_key] || association_class_name.foreign_key - association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation) - association_constructor_method(:build, association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation) - association_constructor_method(:create, association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation) + if options[:polymorphic] + options[:foreign_type] ||= association_class_name.underscore + "_type" - module_eval do - before_save <<-EOF - association = instance_variable_get("@#{association_name}") - if not association.nil? - if association.new_record? - association.save(true) - association.send(:construct_sql) + association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, BelongsToPolymorphicAssociation) + + module_eval do + before_save <<-EOF + association = instance_variable_get("@#{association_name}") + if !association.nil? + if association.new_record? + association.save(true) + association.send(:construct_sql) + end + + if association.updated? + self["#{association_class_primary_key_name}"] = association.id + self["#{options[:foreign_type]}"] = ActiveRecord::Base.send(:class_name_of_active_record_descendant, association.class).to_s + end end - self["#{association_class_primary_key_name}"] = association.id if association.updated? - end - EOF - end + EOF + end + else + require_association_class(association_class_name) + + association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation) + association_constructor_method(:build, association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation) + association_constructor_method(:create, association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation) + + module_eval do + before_save <<-EOF + association = instance_variable_get("@#{association_name}") + if !association.nil? + if association.new_record? + association.save(true) + association.send(:construct_sql) + end + + if association.updated? + self["#{association_class_primary_key_name}"] = association.id + end + end + EOF + end - if options[:counter_cache] - module_eval( - "after_create '#{association_class_name}.increment_counter(\"#{self.to_s.underscore.pluralize + "_count"}\", #{association_class_primary_key_name})" + - " unless #{association_name}.nil?'" - ) + if options[:counter_cache] + module_eval( + "after_create '#{association_class_name}.increment_counter(\"#{self.to_s.underscore.pluralize + "_count"}\", #{association_class_primary_key_name})" + + " unless #{association_name}.nil?'" + ) - module_eval( - "before_destroy '#{association_class_name}.decrement_counter(\"#{self.to_s.underscore.pluralize + "_count"}\", #{association_class_primary_key_name})" + - " unless #{association_name}.nil?'" - ) - end + module_eval( + "before_destroy '#{association_class_name}.decrement_counter(\"#{self.to_s.underscore.pluralize + "_count"}\", #{association_class_primary_key_name})" + + " unless #{association_name}.nil?'" + ) + end - # deprecated api - deprecated_has_association_method(association_name) - deprecated_association_comparison_method(association_name, association_class_name) + # deprecated api + deprecated_has_association_method(association_name) + deprecated_association_comparison_method(association_name, association_class_name) + end end # Associates two classes via an intermediate join table. Unless the join table is explicitly specified as diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 17b8cc6446baeb4ec5da5b8b56b7d3a81cf44ac7..8a7d925d0d2c58770787d3604f4b6750a483f723 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -76,7 +76,6 @@ def extract_options_from_args!(args) end private - def method_missing(method, *args, &block) load_target @target.send(method, *args, &block) diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 528c42cea734738d05f6d8e22cd6d44c932f7cf6..39d128aef194819e5a1e6b63b5afe2a601105eed 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -1,7 +1,6 @@ module ActiveRecord module Associations class BelongsToAssociation < AssociationProxy #:nodoc: - def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options) super construct_sql @@ -43,9 +42,6 @@ def updated? @updated end - protected - - private def find_target if @options[:conditions] diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb new file mode 100644 index 0000000000000000000000000000000000000000..e2a7f1a58e8176182dbbd03608d3b67353eb3c21 --- /dev/null +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -0,0 +1,70 @@ +module ActiveRecord + module Associations + class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc: + def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options) + @owner = owner + @options = options + @association_name = association_name + @association_class_primary_key_name = association_class_primary_key_name + + proxy_extend(options[:extend]) if options[:extend] + + reset + end + + def create(attributes = {}) + raise ActiveRecord::ActiveRecordError, "Can't create an abstract polymorphic object" + end + + def build(attributes = {}) + raise ActiveRecord::ActiveRecordError, "Can't build an abstract polymorphic object" + end + + def replace(obj, dont_save = false) + if obj.nil? + @target = @owner[@association_class_primary_key_name] = @owner[@options[:foreign_type]] = nil + else + @target = (AssociationProxy === obj ? obj.target : obj) + + unless obj.new_record? + @owner[@association_class_primary_key_name] = obj.id + @owner[@options[:foreign_type]] = ActiveRecord::Base.send(:class_name_of_active_record_descendant, obj.class).to_s + end + + @updated = true + end + + @loaded = true + + return (@target.nil? ? nil : self) + end + + private + def find_target + return nil if association_class.nil? + + if @options[:conditions] + association_class.find( + @owner[@association_class_primary_key_name], + :conditions => interpolate_sql(@options[:conditions]), + :include => @options[:include] + ) + else + association_class.find(@owner[@association_class_primary_key_name], :include => @options[:include]) + end + end + + def foreign_key_present + !@owner[@association_class_primary_key_name].nil? + end + + def target_obsolete? + @owner[@association_class_primary_key_name] != @target.id + end + + def association_class + @owner[@options[:foreign_type]] ? @owner[@options[:foreign_type]].constantize : nil + end + end + end +end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 288742d965edbac4d24c0915e8e579f62b66f475..465e1f8c724e460178f7eba9dd04b57510c3b745 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -163,11 +163,19 @@ def target_obsolete? end def construct_sql - if @options[:finder_sql] - @finder_sql = interpolate_sql(@options[:finder_sql]) - else - @finder_sql = "#{@association_class.table_name}.#{@association_class_primary_key_name} = #{@owner.quoted_id}" - @finder_sql << " AND (#{interpolate_sql(@conditions)})" if @conditions + case + when @options[:as] + @finder_sql = + "#{@association_class.table_name}.#{@options[:as]}_id = #{@owner.quoted_id} AND " + + "#{@association_class.table_name}.#{@options[:as]}_type = '#{ActiveRecord::Base.send(:class_name_of_active_record_descendant, @owner.class).to_s}'" + @finder_sql << " AND (#{interpolate_sql(@conditions)})" if @conditions + + when @options[:finder_sql] + @finder_sql = interpolate_sql(@options[:finder_sql]) + + else + @finder_sql = "#{@association_class.table_name}.#{@association_class_primary_key_name} = #{@owner.quoted_id}" + @finder_sql << " AND (#{interpolate_sql(@conditions)})" if @conditions end if @options[:counter_sql] @@ -176,8 +184,7 @@ def construct_sql @options[:counter_sql] = @options[:finder_sql].gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM") @counter_sql = interpolate_sql(@options[:counter_sql]) else - @counter_sql = "#{@association_class.table_name}.#{@association_class_primary_key_name} = #{@owner.quoted_id}" - @counter_sql << " AND (#{interpolate_sql(@conditions)})" if @conditions + @counter_sql = @finder_sql end end end diff --git a/activerecord/test/aaa_create_tables_test.rb b/activerecord/test/aaa_create_tables_test.rb index d49e62561f697d3b8e7e3c3f728958e504dbb166..eb40913930b52a34d751c041bcf071d9b6bce577 100644 --- a/activerecord/test/aaa_create_tables_test.rb +++ b/activerecord/test/aaa_create_tables_test.rb @@ -10,6 +10,11 @@ def test_drop_and_create_main_tables recreate ActiveRecord::Base assert true end + + def test_load_schema + eval(File.read("#{File.dirname(__FILE__)}/fixtures/db_definitions/schema.rb")) + assert true + end def test_drop_and_create_courses_table recreate Course, '2' diff --git a/activerecord/test/associations_interface_test.rb b/activerecord/test/associations_interface_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..5f9447294afa7a3f067e959faef411045beb43db --- /dev/null +++ b/activerecord/test/associations_interface_test.rb @@ -0,0 +1,17 @@ +require 'abstract_unit' +require 'fixtures/tag' +require 'fixtures/tagging' +require 'fixtures/post' +require 'fixtures/comment' + +class AssociationsInterfaceTest < Test::Unit::TestCase + fixtures :posts, :comments, :tags, :taggings + + def test_post_having_a_single_tag_through_has_many + assert_equal taggings(:welcome_general), posts(:welcome).taggings.first + end + + def test_post_having_a_single_tag_through_belongs_to + assert_equal posts(:welcome), posts(:welcome).taggings.first.taggable + end +end diff --git a/activerecord/test/fixtures/db_definitions/schema.rb b/activerecord/test/fixtures/db_definitions/schema.rb new file mode 100644 index 0000000000000000000000000000000000000000..b839edbac08fee67ef80ef0754a0e34adac86dde --- /dev/null +++ b/activerecord/test/fixtures/db_definitions/schema.rb @@ -0,0 +1,13 @@ +ActiveRecord::Schema.define do + + create_table "taggings", :force => true do |t| + t.column "tag_id", :integer + t.column "taggable_type", :string + t.column "taggable_id", :integer + end + + create_table "tags", :force => true do |t| + t.column "name", :string + end + +end \ No newline at end of file diff --git a/activerecord/test/fixtures/post.rb b/activerecord/test/fixtures/post.rb index bf44d8a0a53ceb448c2711186c581003a15aa5c2..61249c43e0b619effa88ac70543bf62911d553b7 100644 --- a/activerecord/test/fixtures/post.rb +++ b/activerecord/test/fixtures/post.rb @@ -19,7 +19,9 @@ def find_most_recent has_and_belongs_to_many :categories has_and_belongs_to_many :special_categories, :join_table => "categories_posts" - + + has_many :taggings, :as => :taggable + def self.what_are_you 'a post...' end diff --git a/activerecord/test/fixtures/tag.rb b/activerecord/test/fixtures/tag.rb new file mode 100644 index 0000000000000000000000000000000000000000..bfd81c69f7a44c950ae3670f942051fb9edf93a3 --- /dev/null +++ b/activerecord/test/fixtures/tag.rb @@ -0,0 +1,2 @@ +class Tag < ActiveRecord::Base +end \ No newline at end of file diff --git a/activerecord/test/fixtures/tagging.rb b/activerecord/test/fixtures/tagging.rb new file mode 100644 index 0000000000000000000000000000000000000000..06d0144b5adbca037817bc4863fefbcb13476397 --- /dev/null +++ b/activerecord/test/fixtures/tagging.rb @@ -0,0 +1,4 @@ +class Tagging < ActiveRecord::Base + belongs_to :tag + belongs_to :taggable, :polymorphic => true +end \ No newline at end of file diff --git a/activerecord/test/fixtures/taggings.yml b/activerecord/test/fixtures/taggings.yml new file mode 100644 index 0000000000000000000000000000000000000000..ca171346f13f4b2d5962c9aa3b7e5694dd7f3d37 --- /dev/null +++ b/activerecord/test/fixtures/taggings.yml @@ -0,0 +1,5 @@ +welcome_general: + id: 1 + tag_id: 1 + taggable_id: 1 + taggable_type: Post diff --git a/activerecord/test/fixtures/tags.yml b/activerecord/test/fixtures/tags.yml new file mode 100644 index 0000000000000000000000000000000000000000..2a494089ff63bf4f303480274cd5985351c1ffa8 --- /dev/null +++ b/activerecord/test/fixtures/tags.yml @@ -0,0 +1,3 @@ +general: + id: 1 + name: General \ No newline at end of file