diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 397a89730aa9f..44f00e0418f39 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 ] diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 9aad0f028d87b..05c6c895019bb 100755 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -38,10 +38,10 @@ 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 6824df9b37140..0970eaceee917 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 @@ def composed_of(part_id, options = {}) 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 86b7101c64304..2fcce6348eafa 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_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 ClassMethods # '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 @@ def has_many(association_id, options = {}, &extension) # 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 @@ def has_one(association_id, options = {}) # belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id", # :conditions => 'discounts > #{payments_count}' def belongs_to(association_id, options = {}) - options.assert_valid_keys(:class_name, :foreign_key, :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 @@ def belongs_to(association_id, options = {}) 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 @@ def belongs_to(association_id, options = {}) # 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 @@ def join_table_name(first_table_name, second_table_name) 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 @@ def add_multiple_associated_save_callbacks(association_name) 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 @@ def find_with_associations(options = {}) 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 7ee567e0b4ade..a66248ff2e0ae 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -18,6 +18,7 @@ def reset def <<(*records) result = true load_target + @owner.transaction do flatten_deeper(records).each do |record| raise_on_type_mismatch(record) @@ -28,7 +29,7 @@ def <<(*records) end end - result and self + result && self end alias_method :push, :<< @@ -60,11 +61,13 @@ def delete(*records) # 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 @@ def replace(other_array) 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 @@ def callback(method, record) 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 8a7d925d0d2c5..75f9184aa21e4 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -5,15 +5,9 @@ class AssociationProxy #:nodoc: 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 @@ def ===(other) other === @target end + def reset + @target = nil + @loaded = false + end + def reload reset load_target @@ -45,14 +44,14 @@ def target @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 @@ def interpolate_sql(sql, record = nil) 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 @@ def method_missing(method, *args, &block) 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 @@ def foreign_key_present 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 39d128aef1948..804a7ebf216ac 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 @@ def updated? 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 e2a7f1a58e817..6c2b9b89fd6d8 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 4bfa75966663e..417b0905f4e3b 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 @@ def find(*args) 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 @@ def find(*args) 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 @@ def insert_record(record) 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 @@ def insert_record(record) 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 @@ def insert_record(record) 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 465e1f8c724e4..f4a08420b7d00 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 @@ def build(attributes = {}) 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 @@ def build(attributes = {}) # 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 @@ def find_first(conditions = nil, orderings = nil) # 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 @@ def find(*args) 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 @@ def find(*args) 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 @@ def find_target 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 @@ def has_cached_counter? 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 @@ def target_obsolete? 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 0000000000000..ca4496b32e464 --- /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 8f7857ebeaa69..17483305fc414 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 @@ def create(attributes = {}, replace_existing = true) 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 @@ def build(attributes = {}, replace_existing = true) 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 @@ def replace(obj, dont_save = false) 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 @@ def replace(obj, dont_save = false) 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 e79dd45d887de..4232b184505ec 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -930,12 +930,17 @@ def type_name_with_module(type_name) 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 @@ def extract_options_from_args!(args) 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 affbb65ca616d..87a05086fce03 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 @@ def #{association_type}_with_reflection(association_id, options = {}, &block) # # 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 @@ def options # 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 @@ def ==(other_aggregation) # 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 @@ def name_to_class_name(name) # 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 similarity index 58% rename from activerecord/test/associations_interface_test.rb rename to activerecord/test/associations_join_model_test.rb index 5f9447294afa7..303c33679faf4 100644 --- a/activerecord/test/associations_interface_test.rb +++ b/activerecord/test/associations_join_model_test.rb @@ -4,14 +4,18 @@ 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 61249c43e0b61..97b25179f50ce 100644 --- a/activerecord/test/fixtures/post.rb +++ b/activerecord/test/fixtures/post.rb @@ -21,6 +21,7 @@ def find_most_recent 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 c3117b8c5dd75..7af9e8c70d093 100644 --- a/activerecord/test/reflection_test.rb +++ b/activerecord/test/reflection_test.rb @@ -60,10 +60,9 @@ def test_aggregation_reflection :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)