1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

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
This commit is contained in:
David Heinemeier Hansson 2005-12-03 04:29:55 +00:00
parent 57b7532b91
commit 6abda696b5
17 changed files with 521 additions and 447 deletions

View file

@ -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>]

View file

@ -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'

View file

@ -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

View file

@ -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.")
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
# 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'
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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
return (@target.nil? ? nil : self)
loaded
record
end
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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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...'

View file

@ -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)