mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Work-in progress for providing better join model support and polymorphic associations
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@3209 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
96c29ab890
commit
57b7532b91
13 changed files with 194 additions and 44 deletions
|
@ -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 @@ module ActiveRecord
|
|||
: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 @@ module ActiveRecord
|
|||
# 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?'"
|
||||
)
|
||||
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)
|
||||
end
|
||||
|
||||
# deprecated api
|
||||
deprecated_has_association_method(association_name)
|
||||
deprecated_association_comparison_method(association_name, association_class_name)
|
||||
end
|
||||
|
||||
# Associates two classes via an intermediate join table. Unless the join table is explicitly specified as
|
||||
|
|
|
@ -76,7 +76,6 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
private
|
||||
|
||||
def method_missing(method, *args, &block)
|
||||
load_target
|
||||
@target.send(method, *args, &block)
|
||||
|
|
|
@ -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 @@ module ActiveRecord
|
|||
@updated
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
|
||||
private
|
||||
def find_target
|
||||
if @options[:conditions]
|
||||
|
|
|
@ -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
|
|
@ -163,11 +163,19 @@ module ActiveRecord
|
|||
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 @@ module ActiveRecord
|
|||
@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
|
||||
|
|
|
@ -10,6 +10,11 @@ class CreateTablesTest < Test::Unit::TestCase
|
|||
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'
|
||||
|
|
17
activerecord/test/associations_interface_test.rb
Normal file
17
activerecord/test/associations_interface_test.rb
Normal file
|
@ -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
|
13
activerecord/test/fixtures/db_definitions/schema.rb
vendored
Normal file
13
activerecord/test/fixtures/db_definitions/schema.rb
vendored
Normal file
|
@ -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
|
4
activerecord/test/fixtures/post.rb
vendored
4
activerecord/test/fixtures/post.rb
vendored
|
@ -19,7 +19,9 @@ class Post < ActiveRecord::Base
|
|||
|
||||
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
|
||||
|
|
2
activerecord/test/fixtures/tag.rb
vendored
Normal file
2
activerecord/test/fixtures/tag.rb
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
class Tag < ActiveRecord::Base
|
||||
end
|
4
activerecord/test/fixtures/tagging.rb
vendored
Normal file
4
activerecord/test/fixtures/tagging.rb
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
class Tagging < ActiveRecord::Base
|
||||
belongs_to :tag
|
||||
belongs_to :taggable, :polymorphic => true
|
||||
end
|
5
activerecord/test/fixtures/taggings.yml
vendored
Normal file
5
activerecord/test/fixtures/taggings.yml
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
welcome_general:
|
||||
id: 1
|
||||
tag_id: 1
|
||||
taggable_id: 1
|
||||
taggable_type: Post
|
3
activerecord/test/fixtures/tags.yml
vendored
Normal file
3
activerecord/test/fixtures/tags.yml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
general:
|
||||
id: 1
|
||||
name: General
|
Loading…
Reference in a new issue