aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Heinemeier Hansson <david@loudthinking.com>2005-12-03 04:29:55 +0000
committerDavid Heinemeier Hansson <david@loudthinking.com>2005-12-03 04:29:55 +0000
commit6abda696b5df14a9ab132c34311daaabe12030e6 (patch)
treeeb0cfad7daa4d46848e6ea10e1abd90ed93a3368
parent57b7532b910f9258cad4111db79349d2d63be6d4 (diff)
downloadrails-6abda696b5df14a9ab132c34311daaabe12030e6.tar.gz
rails-6abda696b5df14a9ab132c34311daaabe12030e6.tar.bz2
rails-6abda696b5df14a9ab132c34311daaabe12030e6.zip
Added preliminary support for join models [DHH] Added preliminary support for polymorphic associations [DHH] Refactored associations to use reflections to get DRYer, beware, major refactoring -- double check before deploying anything with this (all tests pass, but..)
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@3213 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
-rw-r--r--activerecord/CHANGELOG4
-rwxr-xr-xactiverecord/lib/active_record.rb2
-rw-r--r--activerecord/lib/active_record/aggregations.rb5
-rwxr-xr-xactiverecord/lib/active_record/associations.rb383
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb19
-rw-r--r--activerecord/lib/active_record/associations/association_proxy.rb38
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb58
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb62
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb70
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb105
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb80
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb27
-rwxr-xr-xactiverecord/lib/active_record/base.rb9
-rw-r--r--activerecord/lib/active_record/reflection.rb86
-rw-r--r--activerecord/test/associations_join_model_test.rb (renamed from activerecord/test/associations_interface_test.rb)10
-rw-r--r--activerecord/test/fixtures/post.rb1
-rw-r--r--activerecord/test/reflection_test.rb7
17 files changed, 520 insertions, 446 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index 397a89730a..44f00e0418 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,5 +1,9 @@
*SVN*
+* Added preliminary support for polymorphic associations [DHH]
+
+* Added preliminary support for join models [DHH]
+
* Allow validate_uniqueness_of to be scoped by more than just one column. #1559. [jeremy@jthopple.com, Marcel Molina Jr.]
* Firebird: active? and reconnect! methods for handling stale connections. #428 [Ken Kunz <kennethkunz@gmail.com>]
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 9aad0f028d..05c6c89501 100755
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -38,10 +38,10 @@ require 'active_record/base'
require 'active_record/observer'
require 'active_record/validations'
require 'active_record/callbacks'
+require 'active_record/reflection'
require 'active_record/associations'
require 'active_record/aggregations'
require 'active_record/transactions'
-require 'active_record/reflection'
require 'active_record/timestamp'
require 'active_record/acts/list'
require 'active_record/acts/tree'
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 6824df9b37..0970eaceee 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -1,7 +1,6 @@
module ActiveRecord
module Aggregations # :nodoc:
- def self.append_features(base)
- super
+ def self.included(base)
base.extend(ClassMethods)
end
@@ -128,6 +127,8 @@ module ActiveRecord
reader_method(name, class_name, mapping)
writer_method(name, class_name, mapping)
+
+ create_reflection(:composed_of, part_id, options, self)
end
private
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 86b7101c64..2fcce6348e 100755
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -4,6 +4,7 @@ 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_many_through_association'
require 'active_record/associations/has_and_belongs_to_many_association'
require 'active_record/deprecated_associations'
@@ -341,57 +342,19 @@ module ActiveRecord
# 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
# 'ORDER BY p.first_name'
def has_many(association_id, options = {}, &extension)
- options.assert_valid_keys(
- :foreign_key, :class_name, :exclusively_dependent, :dependent,
- :conditions, :order, :include, :finder_sql, :counter_sql,
- :before_add, :after_add, :before_remove, :after_remove, :extend,
- :group, :as
- )
+ reflection = create_has_many_reflection(association_id, options, &extension)
- options[:extend] = create_extension_module(association_id, extension) if block_given?
+ configure_dependency_for_has_many(reflection)
- association_name, association_class_name, association_class_primary_key_name =
- associate_identification(association_id, options[:class_name], options[:foreign_key])
-
- require_association_class(association_class_name)
-
- raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.' if options[:dependent] and options[:exclusively_dependent]
-
- if options[:exclusively_dependent]
- options[:dependent] = :delete_all
- #warn "The :exclusively_dependent option is deprecated. Please use :dependent => :delete_all instead.")
- end
-
- # See HasManyAssociation#delete_records. Dependent associations
- # delete children, otherwise foreign key is set to NULL.
- case options[:dependent]
- when :destroy, true
- module_eval "before_destroy '#{association_name}.each { |o| o.destroy }'"
- when :delete_all
- module_eval "before_destroy { |record| #{association_class_name}.delete_all(%(#{association_class_primary_key_name} = \#{record.quoted_id})) }"
- when :nullify
- module_eval "before_destroy { |record| #{association_class_name}.update_all(%(#{association_class_primary_key_name} = NULL), %(#{association_class_primary_key_name} = \#{record.quoted_id})) }"
- when nil, false
- # pass
- else
- raise ArgumentError, 'The :dependent option expects either true, :destroy, :delete_all, or :nullify'
+ if options[:through]
+ collection_reader_method(reflection, HasManyThroughAssociation)
+ else
+ add_multiple_associated_save_callbacks(reflection.name)
+ add_association_callbacks(reflection.name, reflection.options)
+ collection_accessor_methods(reflection, HasManyAssociation)
end
-
- add_multiple_associated_save_callbacks(association_name)
- add_association_callbacks(association_name, options)
-
- collection_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasManyAssociation)
-
- # deprecated api
- deprecated_collection_count_method(association_name)
- deprecated_add_association_relation(association_name)
- deprecated_remove_association_relation(association_name)
- deprecated_has_collection_method(association_name)
- deprecated_find_in_collection_method(association_name)
- deprecated_find_all_in_collection_method(association_name)
- deprecated_collection_create_method(association_name)
- deprecated_collection_build_method(association_name)
+ add_deprecated_api_for_has_many(reflection.name)
end
# Adds the following methods for retrieval and query of a single associated object.
@@ -436,42 +399,27 @@ module ActiveRecord
# has_one :last_comment, :class_name => "Comment", :order => "posted_on"
# has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
def has_one(association_id, options = {})
- options.assert_valid_keys(:class_name, :foreign_key, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend)
-
- association_name, association_class_name, association_class_primary_key_name =
- associate_identification(association_id, options[:class_name], options[:foreign_key], false)
-
- require_association_class(association_class_name)
+ reflection = create_has_one_reflection(association_id, options)
module_eval do
after_save <<-EOF
- association = instance_variable_get("@#{association_name}")
+ association = instance_variable_get("@#{reflection.name}")
unless association.nil?
- association["#{association_class_primary_key_name}"] = id
+ association["#{reflection.primary_key_name}"] = id
association.save(true)
- association.send(:construct_sql)
end
EOF
end
- association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation)
- association_constructor_method(:build, association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation)
- association_constructor_method(:create, association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation)
+ association_accessor_methods(reflection, HasOneAssociation)
+ association_constructor_method(:build, reflection, HasOneAssociation)
+ association_constructor_method(:create, reflection, HasOneAssociation)
- case options[:dependent]
- when :destroy, true
- module_eval "before_destroy '#{association_name}.destroy unless #{association_name}.nil?'"
- when :nullify
- module_eval "before_destroy '#{association_name}.update_attribute(\"#{association_class_primary_key_name}\", nil)'"
- when nil, false
- # pass
- else
- raise ArgumentError, "The :dependent option expects either :destroy or :nullify."
- end
+ configure_dependency_for_has_one(reflection)
# deprecated api
- deprecated_has_association_method(association_name)
- deprecated_association_comparison_method(association_name, association_class_name)
+ deprecated_has_association_method(reflection.name)
+ deprecated_association_comparison_method(reflection.name, reflection.class_name)
end
# Adds the following methods for retrieval and query for a single associated object that this object holds an id to.
@@ -517,52 +465,41 @@ 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, :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)
-
- association_class_primary_key_name = options[:foreign_key] || association_class_name.foreign_key
-
- if options[:polymorphic]
- options[:foreign_type] ||= association_class_name.underscore + "_type"
-
- association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, BelongsToPolymorphicAssociation)
+ reflection = create_belongs_to_reflection(association_id, options)
+
+ if reflection.options[:polymorphic]
+ association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
module_eval do
before_save <<-EOF
- association = instance_variable_get("@#{association_name}")
+ association = instance_variable_get("@#{reflection.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
+ self["#{reflection.primary_key_name}"] = association.id
+ self["#{reflection.options[:foreign_type]}"] = ActiveRecord::Base.send(:class_name_of_active_record_descendant, association.class).to_s
end
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)
+ association_accessor_methods(reflection, BelongsToAssociation)
+ association_constructor_method(:build, reflection, BelongsToAssociation)
+ association_constructor_method(:create, reflection, BelongsToAssociation)
module_eval do
before_save <<-EOF
- association = instance_variable_get("@#{association_name}")
+ association = instance_variable_get("@#{reflection.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["#{reflection.primary_key_name}"] = association.id
end
end
EOF
@@ -570,19 +507,19 @@ module ActiveRecord
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?'"
+ "after_create '#{reflection.class_name}.increment_counter(\"#{self.to_s.underscore.pluralize + "_count"}\", #{reflection.primary_key_name})" +
+ " unless #{reflection.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?'"
+ "before_destroy '#{reflection.class_name}.decrement_counter(\"#{self.to_s.underscore.pluralize + "_count"}\", #{reflection.primary_key_name})" +
+ " unless #{reflection.name}.nil?'"
)
end
# deprecated api
- deprecated_has_association_method(association_name)
- deprecated_association_comparison_method(association_name, association_class_name)
+ deprecated_has_association_method(reflection.name)
+ deprecated_association_comparison_method(reflection.name, reflection.class_name)
end
end
@@ -663,43 +600,29 @@ module ActiveRecord
# has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql =>
# 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}'
def has_and_belongs_to_many(association_id, options = {}, &extension)
- options.assert_valid_keys(
- :class_name, :table_name, :foreign_key, :association_foreign_key, :conditions, :include,
- :join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq, :before_add, :after_add,
- :before_remove, :after_remove, :extend
- )
-
- options[:extend] = create_extension_module(association_id, extension) if block_given?
-
- association_name, association_class_name, association_class_primary_key_name =
- associate_identification(association_id, options[:class_name], options[:foreign_key])
-
- require_association_class(association_class_name)
-
- options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name))
-
- add_multiple_associated_save_callbacks(association_name)
-
- collection_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasAndBelongsToManyAssociation)
+ reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)
+
+ add_multiple_associated_save_callbacks(reflection.name)
+ collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)
# Don't use a before_destroy callback since users' before_destroy
# callbacks will be executed after the association is wiped out.
- old_method = "destroy_without_habtm_shim_for_#{association_name}"
+ old_method = "destroy_without_habtm_shim_for_#{reflection.name}"
class_eval <<-end_eval
alias_method :#{old_method}, :destroy_without_callbacks
def destroy_without_callbacks
- #{association_name}.clear
+ #{reflection.name}.clear
#{old_method}
end
end_eval
- add_association_callbacks(association_name, options)
+ add_association_callbacks(reflection.name, options)
# deprecated api
- deprecated_collection_count_method(association_name)
- deprecated_add_association_relation(association_name)
- deprecated_remove_association_relation(association_name)
- deprecated_has_collection_method(association_name)
+ deprecated_collection_count_method(reflection.name)
+ deprecated_add_association_relation(reflection.name)
+ deprecated_remove_association_relation(reflection.name)
+ deprecated_has_collection_method(reflection.name)
end
private
@@ -713,93 +636,81 @@ module ActiveRecord
table_name_prefix + join_table + table_name_suffix
end
- def associate_identification(association_id, association_class_name, foreign_key, plural = true)
- if association_class_name !~ /::/
- association_class_name = type_name_with_module(
- association_class_name ||
- Inflector.camelize(plural ? Inflector.singularize(association_id.id2name) : association_id.id2name)
- )
- end
-
- primary_key_name = foreign_key || name.foreign_key
-
- return association_id.id2name, association_class_name, primary_key_name
- end
-
- def association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, association_proxy_class)
- define_method(association_name) do |*params|
+ def association_accessor_methods(reflection, association_proxy_class)
+ define_method(reflection.name) do |*params|
force_reload = params.first unless params.empty?
- association = instance_variable_get("@#{association_name}")
- if association.nil? or force_reload
- association = association_proxy_class.new(self,
- association_name, association_class_name,
- association_class_primary_key_name, options)
+ association = instance_variable_get("@#{reflection.name}")
+
+ if association.nil? || force_reload
+ association = association_proxy_class.new(self, reflection)
retval = association.reload
unless retval.nil?
- instance_variable_set("@#{association_name}", association)
+ instance_variable_set("@#{reflection.name}", association)
else
- instance_variable_set("@#{association_name}", nil)
+ instance_variable_set("@#{reflection.name}", nil)
return nil
end
end
association
end
- define_method("#{association_name}=") do |new_value|
- association = instance_variable_get("@#{association_name}")
+ define_method("#{reflection.name}=") do |new_value|
+ association = instance_variable_get("@#{reflection.name}")
if association.nil?
- association = association_proxy_class.new(self,
- association_name, association_class_name,
- association_class_primary_key_name, options)
+ association = association_proxy_class.new(self, reflection)
end
+
association.replace(new_value)
+
unless new_value.nil?
- instance_variable_set("@#{association_name}", association)
+ instance_variable_set("@#{reflection.name}", association)
else
- instance_variable_set("@#{association_name}", nil)
+ instance_variable_set("@#{reflection.name}", nil)
return nil
end
+
association
end
- define_method("set_#{association_name}_target") do |target|
+ define_method("set_#{reflection.name}_target") do |target|
return if target.nil?
- association = association_proxy_class.new(self,
- association_name, association_class_name,
- association_class_primary_key_name, options)
+ association = association_proxy_class.new(self, reflection)
association.target = target
- instance_variable_set("@#{association_name}", association)
+ instance_variable_set("@#{reflection.name}", association)
end
end
- def collection_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, association_proxy_class)
- define_method(association_name) do |*params|
+ def collection_reader_method(reflection, association_proxy_class)
+ define_method(reflection.name) do |*params|
force_reload = params.first unless params.empty?
- association = instance_variable_get("@#{association_name}")
+ association = instance_variable_get("@#{reflection.name}")
+
unless association.respond_to?(:loaded?)
- association = association_proxy_class.new(self,
- association_name, association_class_name,
- association_class_primary_key_name, options)
- instance_variable_set("@#{association_name}", association)
+ association = association_proxy_class.new(self, reflection)
+ instance_variable_set("@#{reflection.name}", association)
end
+
association.reload if force_reload
+
association
end
+ end
- define_method("#{association_name}=") do |new_value|
- association = instance_variable_get("@#{association_name}")
+ def collection_accessor_methods(reflection, association_proxy_class)
+ collection_reader_method(reflection, association_proxy_class)
+
+ define_method("#{reflection.name}=") do |new_value|
+ association = instance_variable_get("@#{reflection.name}")
unless association.respond_to?(:loaded?)
- association = association_proxy_class.new(self,
- association_name, association_class_name,
- association_class_primary_key_name, options)
- instance_variable_set("@#{association_name}", association)
+ association = association_proxy_class.new(self, reflection)
+ instance_variable_set("@#{reflection.name}", association)
end
association.replace(new_value)
association
end
- define_method("#{Inflector.singularize(association_name)}_ids=") do |new_value|
- send("#{association_name}=", association_class_name.constantize.find(new_value))
+ define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
+ send("#{reflection.name}=", reflection.class_name.constantize.find(new_value))
end
end
@@ -847,17 +758,15 @@ module ActiveRecord
after_update(after_callback)
end
- def association_constructor_method(constructor, association_name, association_class_name, association_class_primary_key_name, options, association_proxy_class)
- define_method("#{constructor}_#{association_name}") do |*params|
+ def association_constructor_method(constructor, reflection, association_proxy_class)
+ define_method("#{constructor}_#{reflection.name}") do |*params|
attributees = params.first unless params.empty?
replace_existing = params[1].nil? ? true : params[1]
- association = instance_variable_get("@#{association_name}")
+ association = instance_variable_get("@#{reflection.name}")
if association.nil?
- association = association_proxy_class.new(self,
- association_name, association_class_name,
- association_class_primary_key_name, options)
- instance_variable_set("@#{association_name}", association)
+ association = association_proxy_class.new(self, reflection)
+ instance_variable_set("@#{reflection.name}", association)
end
if association_proxy_class == HasOneAssociation
@@ -910,6 +819,118 @@ module ActiveRecord
end
+ def configure_dependency_for_has_many(reflection)
+ if reflection.options[:dependent] && reflection.options[:exclusively_dependent]
+ raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.'
+ end
+
+ if reflection.options[:exclusively_dependent]
+ reflection.options[:dependent] = :delete_all
+ #warn "The :exclusively_dependent option is deprecated. Please use :dependent => :delete_all instead.")
+ end
+
+ # See HasManyAssociation#delete_records. Dependent associations
+ # delete children, otherwise foreign key is set to NULL.
+ case reflection.options[:dependent]
+ when :destroy, true
+ module_eval "before_destroy '#{reflection.name}.each { |o| o.destroy }'"
+ when :delete_all
+ module_eval "before_destroy { |record| #{reflection.class_name}.delete_all(%(#{reflection.primary_key_name} = \#{record.quoted_id})) }"
+ when :nullify
+ module_eval "before_destroy { |record| #{reflection.class_name}.update_all(%(#{reflection.primary_key_name} = NULL), %(#{reflection.primary_key_name} = \#{record.quoted_id})) }"
+ when nil, false
+ # pass
+ else
+ raise ArgumentError, 'The :dependent option expects either true, :destroy, :delete_all, or :nullify'
+ end
+ end
+
+ def configure_dependency_for_has_one(reflection)
+ case reflection.options[:dependent]
+ when :destroy, true
+ module_eval "before_destroy '#{reflection.name}.destroy unless #{reflection.name}.nil?'"
+ when :nullify
+ module_eval "before_destroy '#{reflection.name}.update_attribute(\"#{reflection.primary_key_name}\", nil)'"
+ when nil, false
+ # pass
+ else
+ raise ArgumentError, "The :dependent option expects either :destroy or :nullify."
+ end
+ end
+
+
+ def add_deprecated_api_for_has_many(association_name)
+ deprecated_collection_count_method(association_name)
+ deprecated_add_association_relation(association_name)
+ deprecated_remove_association_relation(association_name)
+ deprecated_has_collection_method(association_name)
+ deprecated_find_in_collection_method(association_name)
+ deprecated_find_all_in_collection_method(association_name)
+ deprecated_collection_create_method(association_name)
+ deprecated_collection_build_method(association_name)
+ end
+
+ def create_has_many_reflection(association_id, options, &extension)
+ options.assert_valid_keys(
+ :foreign_key, :class_name, :exclusively_dependent, :dependent,
+ :conditions, :order, :include, :finder_sql, :counter_sql,
+ :before_add, :after_add, :before_remove, :after_remove, :extend,
+ :group, :as, :through
+ )
+
+ options[:extend] = create_extension_module(association_id, extension) if block_given?
+
+ reflection = create_reflection(:has_many, association_id, options, self)
+ reflection.require_class
+
+ reflection
+ end
+
+ def create_has_one_reflection(association_id, options)
+ options.assert_valid_keys(
+ :class_name, :foreign_key, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend
+ )
+
+ reflection = create_reflection(:has_one, association_id, options, self)
+ reflection.require_class
+
+ reflection
+ end
+
+ def create_belongs_to_reflection(association_id, options)
+ options.assert_valid_keys(
+ :class_name, :foreign_key, :foreign_type, :remote, :conditions, :order, :include, :dependent,
+ :counter_cache, :extend, :polymorphic
+ )
+
+ reflection = create_reflection(:belongs_to, association_id, options, self)
+
+ if options[:polymorphic]
+ reflection.options[:foreign_type] ||= reflection.class_name.underscore + "_type"
+ else
+ reflection.require_class
+ end
+
+ reflection
+ end
+
+ def create_has_and_belongs_to_many_reflection(association_id, options, &extension)
+ options.assert_valid_keys(
+ :class_name, :table_name, :foreign_key, :association_foreign_key, :conditions, :include,
+ :join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq, :before_add, :after_add,
+ :before_remove, :after_remove, :extend
+ )
+
+ options[:extend] = create_extension_module(association_id, extension) if block_given?
+
+ reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self)
+ reflection.require_class
+
+ reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name))
+
+ reflection
+ end
+
def reflect_on_included_associations(associations)
[ associations ].flatten.collect { |association| reflect_on_association(association.to_s.intern) }
end
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
index 7ee567e0b4..a66248ff2e 100644
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -18,6 +18,7 @@ module ActiveRecord
def <<(*records)
result = true
load_target
+
@owner.transaction do
flatten_deeper(records).each do |record|
raise_on_type_mismatch(record)
@@ -28,7 +29,7 @@ module ActiveRecord
end
end
- result and self
+ result && self
end
alias_method :push, :<<
@@ -60,11 +61,13 @@ module ActiveRecord
# Removes all records from this association. Returns +self+ so method calls may be chained.
def clear
return self if length.zero? # forces load_target if hasn't happened already
- if @options[:exclusively_dependent]
+
+ if @reflection.options[:exclusively_dependent]
destroy_all
else
delete_all
end
+
self
end
@@ -124,14 +127,6 @@ module ActiveRecord
end
private
- def raise_on_type_mismatch(record)
- raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class)
- end
-
- def target_obsolete?
- false
- end
-
# Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
def flatten_deeper(array)
array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
@@ -155,8 +150,8 @@ module ActiveRecord
end
def callbacks_for(callback_name)
- full_callback_name = "#{callback_name.to_s}_for_#{@association_name.to_s}"
- @owner.class.read_inheritable_attribute(full_callback_name.to_sym) or []
+ full_callback_name = "#{callback_name}_for_#{@reflection.name}"
+ @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
end
end
diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb
index 8a7d925d0d..75f9184aa2 100644
--- a/activerecord/lib/active_record/associations/association_proxy.rb
+++ b/activerecord/lib/active_record/associations/association_proxy.rb
@@ -5,15 +5,9 @@ module ActiveRecord
alias_method :proxy_extend, :extend
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?|^proxy_extend|^send)/ }
- def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
- @owner = owner
- @options = options
- @association_name = association_name
- @association_class = eval(association_class_name, nil, __FILE__, __LINE__)
- @association_class_primary_key_name = association_class_primary_key_name
-
- proxy_extend(options[:extend]) if options[:extend]
-
+ def initialize(owner, reflection)
+ @owner, @reflection = owner, reflection
+ proxy_extend(reflection.options[:extend]) if reflection.options[:extend]
reset
end
@@ -28,6 +22,11 @@ module ActiveRecord
other === @target
end
+ def reset
+ @target = nil
+ @loaded = false
+ end
+
def reload
reset
load_target
@@ -45,14 +44,14 @@ module ActiveRecord
@target
end
- def target=(t)
- @target = t
- @loaded = true
+ def target=(target)
+ @target = target
+ loaded
end
protected
def dependent?
- @options[:dependent] || false
+ @reflection.options[:dependent] || false
end
def quoted_record_ids(records)
@@ -68,7 +67,7 @@ module ActiveRecord
end
def sanitize_sql(sql)
- @association_class.send(:sanitize_sql, sql)
+ @reflection.klass.send(:sanitize_sql, sql)
end
def extract_options_from_args!(args)
@@ -84,13 +83,14 @@ module ActiveRecord
def load_target
if !@owner.new_record? || foreign_key_present
begin
- @target = find_target if not loaded?
+ @target = find_target if !loaded?
rescue ActiveRecord::RecordNotFound
reset
end
end
- @loaded = true if @target
- @target
+
+ loaded if target
+ target
end
# Can be overwritten by associations that might have the foreign key available for an association without
@@ -100,7 +100,9 @@ module ActiveRecord
end
def raise_on_type_mismatch(record)
- raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class)
+ unless record.is_a?(@reflection.klass)
+ raise ActiveRecord::AssociationTypeMismatch, "#{@reflection.class_name} expected, got #{record.class}"
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 39d128aef1..804a7ebf21 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -1,41 +1,27 @@
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
- end
-
- def reset
- @target = nil
- @loaded = false
- end
-
def create(attributes = {})
- record = @association_class.create(attributes)
- replace(record, true)
- record
+ replace(@reflection.klass.create(attributes))
end
def build(attributes = {})
- record = @association_class.new(attributes)
- replace(record, true)
- record
+ replace(@reflection.klass.new(attributes))
end
- def replace(obj, dont_save = false)
- if obj.nil?
- @target = @owner[@association_class_primary_key_name] = nil
+ def replace(record)
+ if record.nil?
+ @target = @owner[@reflection.primary_key_name] = nil
else
- raise_on_type_mismatch(obj) unless obj.nil?
+ raise_on_type_mismatch(record)
- @target = (AssociationProxy === obj ? obj.target : obj)
- @owner[@association_class_primary_key_name] = obj.id unless obj.new_record?
+ @target = (AssociationProxy === record ? record.target : record)
+ @owner[@reflection.primary_key_name] = record.id unless record.new_record?
@updated = true
end
- @loaded = true
- return (@target.nil? ? nil : self)
+ loaded
+ record
end
def updated?
@@ -44,27 +30,15 @@ module ActiveRecord
private
def find_target
- 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
+ @reflection.klass.find(
+ @owner[@reflection.primary_key_name],
+ :conditions => @reflection.options[:conditions] ? interpolate_sql(@reflection.options[:conditions]) : nil,
+ :include => @reflection.options[:include]
+ )
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 construct_sql
- @finder_sql = "#{@association_class.table_name}.#{@association_class.primary_key} = #{@owner.id}"
+ !@owner[@reflection.primary_key_name].nil?
end
end
end
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index e2a7f1a58e..6c2b9b89fd 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -1,69 +1,49 @@
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
+ class BelongsToPolymorphicAssociation < AssociationProxy #:nodoc:
+ def replace(record)
+ if record.nil?
+ @target = @owner[@reflection.primary_key_name] = @owner[@reflection.options[:foreign_type]] = nil
else
- @target = (AssociationProxy === obj ? obj.target : obj)
+ @target = (AssociationProxy === record ? record.target : record)
- 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
+ unless record.new_record?
+ @owner[@reflection.primary_key_name] = record.id
+ @owner[@reflection.options[:foreign_type]] = ActiveRecord::Base.send(:class_name_of_active_record_descendant, record.class).to_s
end
@updated = true
end
- @loaded = true
+ loaded
+ record
+ end
- return (@target.nil? ? nil : self)
+ def updated?
+ @updated
end
-
+
private
def find_target
return nil if association_class.nil?
- if @options[:conditions]
+ if @reflection.options[:conditions]
association_class.find(
- @owner[@association_class_primary_key_name],
- :conditions => interpolate_sql(@options[:conditions]),
- :include => @options[:include]
+ @owner[@reflection.primary_key_name],
+ :conditions => interpolate_sql(@reflection.options[:conditions]),
+ :include => @reflection.options[:include]
)
else
- association_class.find(@owner[@association_class_primary_key_name], :include => @options[:include])
+ association_class.find(@owner[@reflection.primary_key_name], :include => @reflection.options[:include])
end
end
def foreign_key_present
- !@owner[@association_class_primary_key_name].nil?
+ !@owner[@reflection.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
+ @owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
index 4bfa759666..417b0905f4 100644
--- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
@@ -1,20 +1,14 @@
module ActiveRecord
module Associations
class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
- def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
+ def initialize(owner, reflection)
super
-
- @association_foreign_key = options[:association_foreign_key] || association_class_name.foreign_key
- @association_table_name = options[:table_name] || @association_class.table_name
- @join_table = options[:join_table]
- @order = options[:order]
-
construct_sql
end
def build(attributes = {})
load_target
- record = @association_class.new(attributes)
+ record = @reflection.klass.new(attributes)
@target << record
record
end
@@ -27,7 +21,7 @@ module ActiveRecord
options = Base.send(:extract_options_from_args!, args)
# If using a custom finder_sql, scan the entire collection.
- if @options[:finder_sql]
+ if @reflection.options[:finder_sql]
expects_array = args.first.kind_of?(Array)
ids = args.flatten.compact.uniq
@@ -40,60 +34,64 @@ module ActiveRecord
end
else
conditions = "#{@finder_sql}"
+
if sanitized_conditions = sanitize_sql(options[:conditions])
conditions << " AND (#{sanitized_conditions})"
end
+
options[:conditions] = conditions
options[:joins] = @join_sql
options[:readonly] ||= false
- if options[:order] && @options[:order]
- options[:order] = "#{options[:order]}, #{@options[:order]}"
- elsif @options[:order]
- options[:order] = @options[:order]
+ if options[:order] && @reflection.options[:order]
+ options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
+ elsif @reflection.options[:order]
+ options[:order] = @reflection.options[:order]
end
# Pass through args exactly as we received them.
args << options
- @association_class.find(*args)
+ @reflection.klass.find(*args)
end
end
def push_with_attributes(record, join_attributes = {})
raise_on_type_mismatch(record)
join_attributes.each { |key, value| record[key.to_s] = value }
+
callback(:before_add, record)
insert_record(record) unless @owner.new_record?
@target << record
callback(:after_add, record)
+
self
end
alias :concat_with_attributes :push_with_attributes
def size
- @options[:uniq] ? count_records : super
+ @reflection.options[:uniq] ? count_records : super
end
protected
def method_missing(method, *args, &block)
- if @target.respond_to?(method) || (!@association_class.respond_to?(method) && Class.respond_to?(method))
+ if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
super
else
- @association_class.with_scope(:find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false }) do
- @association_class.send(method, *args, &block)
+ @reflection.klass.with_scope(:find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false }) do
+ @reflection.klass.send(method, *args, &block)
end
end
end
def find_target
- if @options[:finder_sql]
- records = @association_class.find_by_sql(@finder_sql)
+ if @reflection.options[:finder_sql]
+ records = @reflection.klass.find_by_sql(@finder_sql)
else
- records = find(:all, :include => @options[:include])
+ records = find(:all, :include => @reflection.options[:include])
end
- @options[:uniq] ? uniq(records) : records
+ @reflection.options[:uniq] ? uniq(records) : records
end
def count_records
@@ -105,16 +103,16 @@ module ActiveRecord
return false unless record.save
end
- if @options[:insert_sql]
- @owner.connection.execute(interpolate_sql(@options[:insert_sql], record))
+ if @reflection.options[:insert_sql]
+ @owner.connection.execute(interpolate_sql(@reflection.options[:insert_sql], record))
else
- columns = @owner.connection.columns(@join_table, "#{@join_table} Columns")
+ columns = @owner.connection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns")
attributes = columns.inject({}) do |attributes, column|
case column.name
- when @association_class_primary_key_name
+ when @reflection.primary_key_name
attributes[column.name] = @owner.quoted_id
- when @association_foreign_key
+ when @reflection.association_foreign_key
attributes[column.name] = record.quoted_id
else
if record.attributes.has_key?(column.name)
@@ -126,7 +124,7 @@ module ActiveRecord
end
sql =
- "INSERT INTO #{@join_table} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " +
+ "INSERT INTO #{@reflection.options[:join_table]} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " +
"VALUES (#{attributes.values.join(', ')})"
@owner.connection.execute(sql)
@@ -136,26 +134,26 @@ module ActiveRecord
end
def delete_records(records)
- if sql = @options[:delete_sql]
+ if sql = @reflection.options[:delete_sql]
records.each { |record| @owner.connection.execute(interpolate_sql(sql, record)) }
else
ids = quoted_record_ids(records)
- sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = #{@owner.quoted_id} AND #{@association_foreign_key} IN (#{ids})"
+ sql = "DELETE FROM #{@reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.association_foreign_key} IN (#{ids})"
@owner.connection.execute(sql)
end
end
def construct_sql
- interpolate_sql_options!(@options, :finder_sql)
+ interpolate_sql_options!(@reflection.options, :finder_sql)
- if @options[:finder_sql]
- @finder_sql = @options[:finder_sql]
+ if @reflection.options[:finder_sql]
+ @finder_sql = @reflection.options[:finder_sql]
else
- @finder_sql = "#{@join_table}.#{@association_class_primary_key_name} = #{@owner.quoted_id} "
- @finder_sql << " AND (#{interpolate_sql(@options[:conditions])})" if @options[:conditions]
+ @finder_sql = "#{@reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{@owner.quoted_id} "
+ @finder_sql << " AND (#{interpolate_sql(@reflection.options[:conditions])})" if @reflection.options[:conditions]
end
- @join_sql = "JOIN #{@join_table} ON #{@association_class.table_name}.#{@association_class.primary_key} = #{@join_table}.#{@association_foreign_key}"
+ @join_sql = "JOIN #{@reflection.options[:join_table]} ON #{@reflection.klass.table_name}.#{@reflection.klass.primary_key} = #{@reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
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 465e1f8c72..f4a08420b7 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -1,10 +1,9 @@
module ActiveRecord
module Associations
class HasManyAssociation < AssociationCollection #:nodoc:
- def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
+ def initialize(owner, reflection)
super
- @conditions = sanitize_sql(options[:conditions])
-
+ @conditions = sanitize_sql(reflection.options[:conditions])
construct_sql
end
@@ -13,8 +12,8 @@ module ActiveRecord
attributes.collect { |attr| create(attr) }
else
load_target
- record = @association_class.new(attributes)
- record[@association_class_primary_key_name] = @owner.id unless @owner.new_record?
+ record = @reflection.klass.new(attributes)
+ record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
@target << record
record
end
@@ -22,13 +21,13 @@ module ActiveRecord
# DEPRECATED.
def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil)
- if @options[:finder_sql]
- @association_class.find_by_sql(@finder_sql)
+ if @reflection.options[:finder_sql]
+ @reflection.klass.find_by_sql(@finder_sql)
else
conditions = @finder_sql
conditions += " AND (#{sanitize_sql(runtime_conditions)})" if runtime_conditions
- orderings ||= @options[:order]
- @association_class.find_all(conditions, orderings, limit, joins)
+ orderings ||= @reflection.options[:order]
+ @reflection.klass.find_all(conditions, orderings, limit, joins)
end
end
@@ -39,14 +38,14 @@ module ActiveRecord
# Count the number of associated records. All arguments are optional.
def count(runtime_conditions = nil)
- if @options[:counter_sql]
- @association_class.count_by_sql(@counter_sql)
- elsif @options[:finder_sql]
- @association_class.count_by_sql(@finder_sql)
+ if @reflection.options[:counter_sql]
+ @reflection.klass.count_by_sql(@counter_sql)
+ elsif @reflection.options[:finder_sql]
+ @reflection.klass.count_by_sql(@finder_sql)
else
sql = @finder_sql
sql += " AND (#{sanitize_sql(runtime_conditions)})" if runtime_conditions
- @association_class.count(sql)
+ @reflection.klass.count(sql)
end
end
@@ -54,7 +53,7 @@ module ActiveRecord
options = Base.send(:extract_options_from_args!, args)
# If using a custom finder_sql, scan the entire collection.
- if @options[:finder_sql]
+ if @reflection.options[:finder_sql]
expects_array = args.first.kind_of?(Array)
ids = args.flatten.compact.uniq
@@ -72,49 +71,49 @@ module ActiveRecord
end
options[:conditions] = conditions
- if options[:order] && @options[:order]
- options[:order] = "#{options[:order]}, #{@options[:order]}"
- elsif @options[:order]
- options[:order] = @options[:order]
+ if options[:order] && @reflection.options[:order]
+ options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
+ elsif @reflection.options[:order]
+ options[:order] = @reflection.options[:order]
end
# Pass through args exactly as we received them.
args << options
- @association_class.find(*args)
+ @reflection.klass.find(*args)
end
end
protected
def method_missing(method, *args, &block)
- if @target.respond_to?(method) || (!@association_class.respond_to?(method) && Class.respond_to?(method))
+ if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
super
else
- @association_class.with_scope(
+ @reflection.klass.with_scope(
:find => {
:conditions => @finder_sql,
:joins => @join_sql,
:readonly => false
},
:create => {
- @association_class_primary_key_name => @owner.id
+ @reflection.primary_key_name => @owner.id
}
) do
- @association_class.send(method, *args, &block)
+ @reflection.klass.send(method, *args, &block)
end
end
end
def find_target
- if @options[:finder_sql]
- @association_class.find_by_sql(@finder_sql)
+ if @reflection.options[:finder_sql]
+ @reflection.klass.find_by_sql(@finder_sql)
else
- @association_class.find(:all,
+ @reflection.klass.find(:all,
:conditions => @finder_sql,
- :order => @options[:order],
- :limit => @options[:limit],
- :joins => @options[:joins],
- :include => @options[:include],
- :group => @options[:group]
+ :order => @reflection.options[:order],
+ :limit => @reflection.options[:limit],
+ :joins => @reflection.options[:joins],
+ :include => @reflection.options[:include],
+ :group => @reflection.options[:group]
)
end
end
@@ -122,10 +121,10 @@ module ActiveRecord
def count_records
count = if has_cached_counter?
@owner.send(:read_attribute, cached_counter_attribute_name)
- elsif @options[:counter_sql]
- @association_class.count_by_sql(@counter_sql)
+ elsif @reflection.options[:counter_sql]
+ @reflection.klass.count_by_sql(@counter_sql)
else
- @association_class.count(@counter_sql)
+ @reflection.klass.count(@counter_sql)
end
@target = [] and loaded if count == 0
@@ -138,22 +137,22 @@ module ActiveRecord
end
def cached_counter_attribute_name
- "#{@association_name}_count"
+ "#{@reflection.name}_count"
end
def insert_record(record)
- record[@association_class_primary_key_name] = @owner.id
+ record[@reflection.primary_key_name] = @owner.id
record.save
end
def delete_records(records)
- if @options[:dependent]
+ if @reflection.options[:dependent]
records.each { |r| r.destroy }
else
ids = quoted_record_ids(records)
- @association_class.update_all(
- "#{@association_class_primary_key_name} = NULL",
- "#{@association_class_primary_key_name} = #{@owner.quoted_id} AND #{@association_class.primary_key} IN (#{ids})"
+ @reflection.klass.update_all(
+ "#{@reflection.primary_key_name} = NULL",
+ "#{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})"
)
end
end
@@ -164,25 +163,25 @@ module ActiveRecord
def construct_sql
case
- when @options[:as]
+ when @reflection.options[:finder_sql]
+ @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
+
+ when @reflection.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}'"
+ "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
+ "#{@reflection.klass.table_name}.#{@reflection.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 = "#{@reflection.klass.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
@finder_sql << " AND (#{interpolate_sql(@conditions)})" if @conditions
end
- if @options[:counter_sql]
- @counter_sql = interpolate_sql(@options[:counter_sql])
- elsif @options[:finder_sql]
- @options[:counter_sql] = @options[:finder_sql].gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM")
- @counter_sql = interpolate_sql(@options[:counter_sql])
+ if @reflection.options[:counter_sql]
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
+ elsif @reflection.options[:finder_sql]
+ @reflection.options[:counter_sql] = @reflection.options[:finder_sql].gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM")
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
else
@counter_sql = @finder_sql
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
new file mode 100644
index 0000000000..ca4496b32e
--- /dev/null
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -0,0 +1,80 @@
+module ActiveRecord
+ module Associations
+ class HasManyThroughAssociation < AssociationProxy #:nodoc:
+ def find(*args)
+ options = Base.send(:extract_options_from_args!, args)
+
+ conditions = "#{@finder_sql}"
+ if sanitized_conditions = sanitize_sql(options[:conditions])
+ conditions << " AND (#{sanitized_conditions})"
+ end
+ options[:conditions] = conditions
+
+ if options[:order] && @reflection.options[:order]
+ options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
+ elsif @reflection.options[:order]
+ options[:order] = @reflection.options[:order]
+ end
+
+ # Pass through args exactly as we received them.
+ args << options
+ @reflection.klass.find(*args)
+ end
+
+ def reset
+ @target = []
+ @loaded = false
+ end
+
+ protected
+ def method_missing(method, *args, &block)
+ if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
+ super
+ else
+ @reflection.klass.with_scope(construct_scope) { @reflection.klass.send(method, *args, &block) }
+ end
+ end
+
+ def find_target
+ @reflection.klass.find(:all,
+ :conditions => construct_conditions,
+ :from => construct_from,
+ :order => @reflection.options[:order],
+ :limit => @reflection.options[:limit],
+ :joins => @reflection.options[:joins],
+ :group => @reflection.options[:group]
+ )
+ end
+
+ def construct_conditions
+ through_reflection = @owner.class.reflections[@reflection.options[:through]]
+
+ if through_reflection.options[:as]
+ conditions =
+ "#{@reflection.table_name}.#{@reflection.klass.primary_key} = #{through_reflection.table_name}.#{@reflection.klass.to_s.foreign_key} " +
+ "AND #{through_reflection.table_name}.#{through_reflection.options[:as]}_id = #{@owner.quoted_id} " +
+ "AND #{through_reflection.table_name}.#{through_reflection.options[:as]}_type = '#{ActiveRecord::Base.send(:class_name_of_active_record_descendant, @owner.class).to_s}'"
+ else
+ conditions =
+ "#{@reflection.klass.table_name}.#{@reflection.klass.primary_key} = #{through_reflection.table_name}.#{@reflection.klass.to_s.foreign_key} " +
+ "AND #{through_reflection.table_name}.#{@owner.to_s.foreign_key} = #{@owner.quoted_id}"
+ end
+
+ conditions << " AND (#{interpolate_sql(sanitize_sql(@reflection.options[:conditions]))})" if @reflection.options[:conditions]
+
+ return conditions
+ end
+
+ def construct_from
+ "#{@reflection.table_name}, #{@owner.class.reflections[@reflection.options[:through]].table_name}"
+ end
+
+ def construct_scope
+ {
+ :find => { :conditions => construct_conditions },
+ :create => { @reflection.primary_key_name => @owner.id }
+ }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 8f7857ebea..17483305fc 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -1,9 +1,8 @@
module ActiveRecord
module Associations
class HasOneAssociation < BelongsToAssociation #:nodoc:
- def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
+ def initialize(owner, reflection)
super
-
construct_sql
end
@@ -14,12 +13,12 @@ module ActiveRecord
end
def build(attributes = {}, replace_existing = true)
- record = @association_class.new(attributes)
+ record = @reflection.klass.new(attributes)
if replace_existing
replace(record, true)
else
- record[@association_class_primary_key_name] = @owner.id unless @owner.new_record?
+ record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
self.target = record
end
@@ -28,12 +27,13 @@ module ActiveRecord
def replace(obj, dont_save = false)
load_target
+
unless @target.nil?
if dependent? && !dont_save && @target != obj
@target.destroy unless @target.new_record?
@owner.clear_association_cache
else
- @target[@association_class_primary_key_name] = nil
+ @target[@reflection.primary_key_name] = nil
@target.save unless @owner.new_record?
end
end
@@ -43,11 +43,12 @@ module ActiveRecord
else
raise_on_type_mismatch(obj)
- obj[@association_class_primary_key_name] = @owner.id unless @owner.new_record?
+ obj[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
@target = (AssociationProxy === obj ? obj.target : obj)
end
@loaded = true
+
unless @owner.new_record? or obj.nil? or dont_save
return (obj.save ? self : false)
else
@@ -57,16 +58,16 @@ module ActiveRecord
private
def find_target
- @association_class.find(:first, :conditions => @finder_sql, :order => @options[:order], :include => @options[:include])
- end
-
- def target_obsolete?
- false
+ @reflection.klass.find(:first,
+ :conditions => @finder_sql,
+ :order => @reflection.options[:order],
+ :include => @reflection.options[:include]
+ )
end
def construct_sql
- @finder_sql = "#{@association_class.table_name}.#{@association_class_primary_key_name} = #{@owner.quoted_id}"
- @finder_sql << " AND (#{sanitize_sql(@options[:conditions])})" if @options[:conditions]
+ @finder_sql = "#{@reflection.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
+ @finder_sql << " AND (#{sanitize_sql(@reflection.options[:conditions])})" if @reflection.options[:conditions]
@finder_sql
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index e79dd45d88..4232b18450 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -930,12 +930,17 @@ module ActiveRecord #:nodoc:
end
def construct_finder_sql(options)
- sql = "SELECT #{options[:select] || '*'} FROM #{table_name} "
+ sql = "SELECT #{options[:select] || '*'} "
+ sql << "FROM #{options[:from] || table_name} "
+
add_joins!(sql, options)
add_conditions!(sql, options[:conditions])
+
sql << " GROUP BY #{options[:group]} " if options[:group]
sql << " ORDER BY #{options[:order]} " if options[:order]
+
add_limit!(sql, options)
+
sql
end
@@ -1180,7 +1185,7 @@ module ActiveRecord #:nodoc:
end
def validate_find_options(options)
- options.assert_valid_keys [:conditions, :include, :joins, :limit, :offset, :order, :select, :readonly, :group]
+ options.assert_valid_keys [:conditions, :include, :joins, :limit, :offset, :order, :select, :readonly, :group, :from]
end
def encode_quoted_value(value)
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index affbb65ca6..87a05086fc 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -1,36 +1,7 @@
module ActiveRecord
module Reflection # :nodoc:
- def self.append_features(base)
- super
+ def self.included(base)
base.extend(ClassMethods)
-
- base.class_eval do
- class << self
- alias_method :composed_of_without_reflection, :composed_of
-
- def composed_of_with_reflection(part_id, options = {})
- composed_of_without_reflection(part_id, options)
- reflect_on_all_aggregations << AggregateReflection.new(:composed_of, part_id, options, self)
- end
-
- alias_method :composed_of, :composed_of_with_reflection
- end
- end
-
- for association_type in %w( belongs_to has_one has_many has_and_belongs_to_many )
- base.module_eval <<-"end_eval"
- class << self
- alias_method :#{association_type}_without_reflection, :#{association_type}
-
- def #{association_type}_with_reflection(association_id, options = {}, &block)
- #{association_type}_without_reflection(association_id, options, &block)
- reflect_on_all_associations << AssociationReflection.new(:#{association_type}, association_id, options, self)
- end
-
- alias_method :#{association_type}, :#{association_type}_with_reflection
- end
- end_eval
- end
end
# Reflection allows you to interrogate Active Record classes and objects about their associations and aggregations.
@@ -39,26 +10,39 @@ module ActiveRecord
#
# You can find the interface for the AggregateReflection and AssociationReflection classes in the abstract MacroReflection class.
module ClassMethods
+ def create_reflection(macro, name, options, active_record)
+ case macro
+ when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
+ reflections[name] = AssociationReflection.new(macro, name, options, active_record)
+ when :composed_of
+ reflections[name] = AggregateReflection.new(macro, name, options, active_record)
+ end
+ end
+
+ def reflections
+ read_inheritable_attribute(:reflections) or write_inheritable_attribute(:reflections, {})
+ end
+
# Returns an array of AggregateReflection objects for all the aggregations in the class.
def reflect_on_all_aggregations
- read_inheritable_attribute(:aggregations) or write_inheritable_attribute(:aggregations, [])
+ reflections.values.select { |reflection| reflection.is_a?(AggregateReflection) }
end
# Returns the AggregateReflection object for the named +aggregation+ (use the symbol). Example:
# Account.reflect_on_aggregation(:balance) # returns the balance AggregateReflection
def reflect_on_aggregation(aggregation)
- reflect_on_all_aggregations.find { |reflection| reflection.name == aggregation } unless reflect_on_all_aggregations.nil?
+ reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil
end
# Returns an array of AssociationReflection objects for all the aggregations in the class.
def reflect_on_all_associations
- read_inheritable_attribute(:associations) or write_inheritable_attribute(:associations, [])
+ reflections.values.select { |reflection| reflection.is_a?(AssociationReflection) }
end
# Returns the AssociationReflection object for the named +aggregation+ (use the symbol). Example:
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
def reflect_on_association(association)
- reflect_on_all_associations.find { |reflection| reflection.name == association }
+ reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil
end
end
@@ -92,6 +76,14 @@ module ActiveRecord
# Returns the class for the macro, so "composed_of :balance, :class_name => 'Money'" would return the Money class and
# "has_many :clients" would return the Client class.
def klass() end
+
+ def class_name
+ @class_name ||= name_to_class_name(name.id2name)
+ end
+
+ def require_class
+ require_association(class_name.underscore) if class_name
+ end
def ==(other_aggregation)
name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
@@ -102,7 +94,7 @@ module ActiveRecord
# Holds all the meta-data about an aggregation as it was specified in the Active Record class.
class AggregateReflection < MacroReflection #:nodoc:
def klass
- Object.const_get(options[:class_name] || name_to_class_name(name.id2name))
+ @klass ||= Object.const_get(class_name)
end
private
@@ -114,22 +106,40 @@ module ActiveRecord
# Holds all the meta-data about an association as it was specified in the Active Record class.
class AssociationReflection < MacroReflection #:nodoc:
def klass
- @klass ||= active_record.send(:compute_type, (name_to_class_name(name.id2name)))
+ @klass ||= active_record.send(:compute_type, class_name)
end
def table_name
@table_name ||= klass.table_name
end
+ def primary_key_name
+ return @primary_key_name if @primary_key_name
+
+ case macro
+ when :belongs_to
+ @primary_key_name = options[:foreign_key] || class_name.foreign_key
+ else
+ @primary_key_name = options[:foreign_key] || active_record.name.foreign_key
+ end
+ end
+
+ def association_foreign_key
+ @association_foreign_key ||= @options[:association_foreign_key] || class_name.foreign_key
+ end
+
private
def name_to_class_name(name)
if name =~ /::/
name
else
- unless class_name = options[:class_name]
+ if options[:class_name]
+ class_name = options[:class_name]
+ else
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)
end
+
active_record.send(:type_name_with_module, class_name)
end
end
diff --git a/activerecord/test/associations_interface_test.rb b/activerecord/test/associations_join_model_test.rb
index 5f9447294a..303c33679f 100644
--- a/activerecord/test/associations_interface_test.rb
+++ b/activerecord/test/associations_join_model_test.rb
@@ -4,14 +4,18 @@ require 'fixtures/tagging'
require 'fixtures/post'
require 'fixtures/comment'
-class AssociationsInterfaceTest < Test::Unit::TestCase
+class AssociationsJoinModelTest < Test::Unit::TestCase
fixtures :posts, :comments, :tags, :taggings
- def test_post_having_a_single_tag_through_has_many
+ def test_polymorphic_has_many
assert_equal taggings(:welcome_general), posts(:welcome).taggings.first
end
- def test_post_having_a_single_tag_through_belongs_to
+ def test_polymorphic_belongs_to
assert_equal posts(:welcome), posts(:welcome).taggings.first.taggable
end
+
+ def test_polymorphic_has_many_going_through_join_model
+ assert_equal tags(:general), posts(:welcome).tags.first
+ end
end
diff --git a/activerecord/test/fixtures/post.rb b/activerecord/test/fixtures/post.rb
index 61249c43e0..97b25179f5 100644
--- a/activerecord/test/fixtures/post.rb
+++ b/activerecord/test/fixtures/post.rb
@@ -21,6 +21,7 @@ class Post < ActiveRecord::Base
has_and_belongs_to_many :special_categories, :join_table => "categories_posts"
has_many :taggings, :as => :taggable
+ has_many :tags, :through => :taggings
def self.what_are_you
'a post...'
diff --git a/activerecord/test/reflection_test.rb b/activerecord/test/reflection_test.rb
index c3117b8c5d..7af9e8c70d 100644
--- a/activerecord/test/reflection_test.rb
+++ b/activerecord/test/reflection_test.rb
@@ -60,10 +60,9 @@ class ReflectionTest < Test::Unit::TestCase
:composed_of, :gps_location, { }, Customer
)
- assert_equal(
- [ reflection_for_address, reflection_for_balance, reflection_for_gps_location ],
- Customer.reflect_on_all_aggregations
- )
+ assert Customer.reflect_on_all_aggregations.include?(reflection_for_gps_location)
+ assert Customer.reflect_on_all_aggregations.include?(reflection_for_balance)
+ assert Customer.reflect_on_all_aggregations.include?(reflection_for_address)
assert_equal reflection_for_address, Customer.reflect_on_aggregation(:address)