diff options
25 files changed, 776 insertions, 620 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 364a7248d2..e0b4db498d 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -107,8 +107,8 @@ module ActiveRecord # (has_many, has_one) when there is at least 1 child associated instance. # ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project class DeleteRestrictionError < ActiveRecordError #:nodoc: - def initialize(reflection) - super("Cannot delete record because of dependent #{reflection.name}") + def initialize(name) + super("Cannot delete record because of dependent #{name}") end end @@ -132,6 +132,17 @@ module ActiveRecord autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association' autoload :ThroughAssociation, 'active_record/associations/through_association' + module Builder #:nodoc: + autoload :Association, 'active_record/associations/builder/association' + autoload :SingularAssociation, 'active_record/associations/builder/singular_association' + autoload :CollectionAssociation, 'active_record/associations/builder/collection_association' + + autoload :BelongsTo, 'active_record/associations/builder/belongs_to' + autoload :HasOne, 'active_record/associations/builder/has_one' + autoload :HasMany, 'active_record/associations/builder/has_many' + autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many' + end + # Clears out the association cache. def clear_association_cache #:nodoc: @association_cache.clear if persisted? @@ -156,7 +167,7 @@ module ActiveRecord private # Returns the specified association instance if it responds to :loaded?, nil otherwise. def association_instance_get(name) - @association_cache[name] + @association_cache[name.to_sym] end # Set the specified association instance. @@ -1108,11 +1119,8 @@ module ActiveRecord # 'FROM people p, post_subscriptions ps ' + # 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' + # 'ORDER BY p.first_name' - def has_many(association_id, options = {}, &extension) - reflection = create_has_many_reflection(association_id, options, &extension) - configure_dependency_for_has_many(reflection) - add_association_callbacks(reflection.name, reflection.options) - collection_accessor_methods(reflection) + def has_many(name, options = {}, &extension) + Builder::HasMany.build(self, name, options, &extension) end # Specifies a one-to-one association with another class. This method should only be used @@ -1224,15 +1232,8 @@ module ActiveRecord # has_one :boss, :readonly => :true # has_one :club, :through => :membership # has_one :primary_address, :through => :addressables, :conditions => ["addressable.primary = ?", true], :source => :addressable - def has_one(association_id, options = {}) - if options[:through] - reflection = create_has_one_through_reflection(association_id, options) - else - reflection = create_has_one_reflection(association_id, options) - association_constructor_methods(reflection) - configure_dependency_for_has_one(reflection) - end - association_accessor_methods(reflection) + def has_one(name, options = {}) + Builder::HasOne.build(self, name, options) end # Specifies a one-to-one association with another class. This method should only be used @@ -1350,19 +1351,8 @@ module ActiveRecord # belongs_to :post, :counter_cache => true # belongs_to :company, :touch => true # belongs_to :company, :touch => :employees_last_updated_at - def belongs_to(association_id, options = {}) - reflection = create_belongs_to_reflection(association_id, options) - - association_accessor_methods(reflection) - - unless reflection.options[:polymorphic] - association_constructor_methods(reflection) - end - - add_counter_cache_callbacks(reflection) if options[:counter_cache] - add_touch_callbacks(reflection, options[:touch]) if options[:touch] - - configure_dependency_for_belongs_to(reflection) + def belongs_to(name, options = {}) + Builder::BelongsTo.build(self, name, options) end # Specifies a many-to-many relationship with another class. This associates two classes via an @@ -1533,364 +1523,9 @@ module ActiveRecord # has_and_belongs_to_many :categories, :readonly => true # 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) - reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension) - collection_accessor_methods(reflection) - - # Don't use a before_destroy callback since users' before_destroy - # callbacks will be executed after the association is wiped out. - include Module.new { - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def destroy # def destroy - super # super - #{reflection.name}.clear # posts.clear - end # end - RUBY - } - - add_association_callbacks(reflection.name, options) + def has_and_belongs_to_many(name, options = {}, &extension) + Builder::HasAndBelongsToMany.build(self, name, options, &extension) end - - private - # Generates a join table name from two provided table names. - # The names in the join table names end up in lexicographic order. - # - # join_table_name("members", "clubs") # => "clubs_members" - # join_table_name("members", "special_clubs") # => "members_special_clubs" - def join_table_name(first_table_name, second_table_name) - if first_table_name < second_table_name - join_table = "#{first_table_name}_#{second_table_name}" - else - join_table = "#{second_table_name}_#{first_table_name}" - end - - table_name_prefix + join_table + table_name_suffix - end - - def association_accessor_methods(reflection) - redefine_method(reflection.name) do |*params| - force_reload = params.first unless params.empty? - association = association(reflection.name) - - if force_reload - reflection.klass.uncached { association.reload } - elsif !association.loaded? || association.stale_target? - association.reload - end - - association.target - end - - redefine_method("#{reflection.name}=") do |record| - association(reflection.name).replace(record) - end - end - - def collection_reader_method(reflection) - redefine_method(reflection.name) do |*params| - force_reload = params.first unless params.empty? - association = association(reflection.name) - - if force_reload - reflection.klass.uncached { association.reload } - elsif association.stale_target? - association.reload - end - - association.proxy - end - - redefine_method("#{reflection.name.to_s.singularize}_ids") do - if send(reflection.name).loaded? || reflection.options[:finder_sql] - records = send(reflection.name) - records.map { |r| r.send(reflection.association_primary_key) } - else - column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" - records = send(reflection.name).select(column).except(:includes) - records.map! { |r| r.send(reflection.association_primary_key) } - end - end - end - - def collection_accessor_methods(reflection, writer = true) - collection_reader_method(reflection) - - if writer - redefine_method("#{reflection.name}=") do |new_value| - association(reflection.name).replace(new_value) - end - - redefine_method("#{reflection.name.to_s.singularize}_ids=") do |new_value| - pk_column = reflection.primary_key_column - ids = (new_value || []).reject { |nid| nid.blank? } - ids.map!{ |i| pk_column.type_cast(i) } - send("#{reflection.name}=", reflection.klass.find(ids).index_by{ |r| r.id }.values_at(*ids)) - end - end - end - - def association_constructor_methods(reflection) - constructors = { - "build_#{reflection.name}" => "build", - "create_#{reflection.name}" => "create", - "create_#{reflection.name}!" => "create!" - } - - constructors.each do |name, proxy_name| - redefine_method(name) do |*params| - attributes = params.first unless params.empty? - association(reflection.name).send(proxy_name, attributes) - end - end - end - - def add_counter_cache_callbacks(reflection) - cache_column = reflection.counter_cache_column - - method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym - define_method(method_name) do - association = send(reflection.name) - association.class.increment_counter(cache_column, association.id) unless association.nil? - end - after_create(method_name) - - method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym - define_method(method_name) do - association = send(reflection.name) - association.class.decrement_counter(cache_column, association.id) unless association.nil? - end - before_destroy(method_name) - - module_eval( - "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)", __FILE__, __LINE__ - ) - end - - def add_touch_callbacks(reflection, touch_attribute) - method_name = :"belongs_to_touch_after_save_or_destroy_for_#{reflection.name}" - redefine_method(method_name) do - association = send(reflection.name) - - if touch_attribute == true - association.touch unless association.nil? - else - association.touch(touch_attribute) unless association.nil? - end - end - after_save(method_name) - after_touch(method_name) - after_destroy(method_name) - end - - # Creates before_destroy callback methods that nullify, delete or destroy - # has_many associated objects, according to the defined :dependent rule. - # - # See HasManyAssociation#delete_records for more information. In general - # - delete children if the option is set to :destroy or :delete_all - # - set the foreign key to NULL if the option is set to :nullify - # - do not delete the parent record if there is any child record if the - # option is set to :restrict - def configure_dependency_for_has_many(reflection) - if reflection.options[:dependent] - method_name = "has_many_dependent_for_#{reflection.name}" - - case reflection.options[:dependent] - when :destroy, :delete_all, :nullify - define_method(method_name) do - if reflection.options[:dependent] == :destroy - send(reflection.name).each do |o| - # No point in executing the counter update since we're going to destroy the parent anyway - counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym - if o.respond_to?(counter_method) - class << o - self - end.send(:define_method, counter_method, Proc.new {}) - end - end - end - - # AssociationProxy#delete_all looks at the :dependent option and acts accordingly - send(reflection.name).delete_all - end - when :restrict - define_method(method_name) do - unless send(reflection.name).empty? - raise DeleteRestrictionError.new(reflection) - end - end - else - raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, :nullify or :restrict (#{reflection.options[:dependent].inspect})" - end - - before_destroy method_name - end - end - - # Creates before_destroy callback methods that nullify, delete or destroy - # has_one associated objects, according to the defined :dependent rule. - # If the association is marked as :dependent => :restrict, create a callback - # that prevents deleting entirely. - def configure_dependency_for_has_one(reflection) - if reflection.options.include?(:dependent) - name = reflection.options[:dependent] - method_name = :"has_one_dependent_#{name}_for_#{reflection.name}" - - case name - when :destroy, :delete - class_eval <<-eoruby, __FILE__, __LINE__ + 1 - def #{method_name} - association = #{reflection.name} - association.#{name} if association - end - eoruby - when :nullify - class_eval <<-eoruby, __FILE__, __LINE__ + 1 - def #{method_name} - association = #{reflection.name} - association.update_attribute(#{reflection.foreign_key.inspect}, nil) if association - end - eoruby - when :restrict - method_name = "has_one_dependent_restrict_for_#{reflection.name}".to_sym - define_method(method_name) do - unless send(reflection.name).nil? - raise DeleteRestrictionError.new(reflection) - end - end - before_destroy method_name - else - raise ArgumentError, "The :dependent option expects either :destroy, :delete, :nullify or :restrict (#{reflection.options[:dependent].inspect})" - end - - before_destroy method_name - end - end - - def configure_dependency_for_belongs_to(reflection) - if reflection.options.include?(:dependent) - name = reflection.options[:dependent] - - unless [:destroy, :delete].include?(name) - raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{reflection.options[:dependent].inspect})" - end - - method_name = :"belongs_to_dependent_#{name}_for_#{reflection.name}" - class_eval <<-eoruby, __FILE__, __LINE__ + 1 - def #{method_name} - association = #{reflection.name} - association.#{name} if association - end - eoruby - after_destroy method_name - end - end - - mattr_accessor :valid_keys_for_has_many_association - @@valid_keys_for_has_many_association = [ - :class_name, :table_name, :foreign_key, :primary_key, - :dependent, - :select, :conditions, :include, :order, :group, :having, :limit, :offset, - :as, :through, :source, :source_type, - :uniq, - :finder_sql, :counter_sql, - :before_add, :after_add, :before_remove, :after_remove, - :extend, :readonly, - :validate, :inverse_of - ] - - def create_has_many_reflection(association_id, options, &extension) - options.assert_valid_keys(valid_keys_for_has_many_association) - options[:extend] = create_extension_modules(association_id, extension, options[:extend]) - - create_reflection(:has_many, association_id, options, self) - end - - mattr_accessor :valid_keys_for_has_one_association - @@valid_keys_for_has_one_association = [ - :class_name, :foreign_key, :remote, :select, :conditions, :order, - :include, :dependent, :counter_cache, :extend, :as, :readonly, - :validate, :primary_key, :inverse_of - ] - - def create_has_one_reflection(association_id, options) - options.assert_valid_keys(valid_keys_for_has_one_association) - create_reflection(:has_one, association_id, options, self) - end - - def create_has_one_through_reflection(association_id, options) - options.assert_valid_keys( - :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source, :source_type, :validate - ) - create_reflection(:has_one, association_id, options, self) - end - - mattr_accessor :valid_keys_for_belongs_to_association - @@valid_keys_for_belongs_to_association = [ - :class_name, :primary_key, :foreign_key, :foreign_type, :remote, :select, :conditions, - :include, :dependent, :counter_cache, :extend, :polymorphic, :readonly, - :validate, :touch, :inverse_of - ] - - def create_belongs_to_reflection(association_id, options) - options.assert_valid_keys(valid_keys_for_belongs_to_association) - create_reflection(:belongs_to, association_id, options, self) - end - - mattr_accessor :valid_keys_for_has_and_belongs_to_many_association - @@valid_keys_for_has_and_belongs_to_many_association = [ - :class_name, :table_name, :join_table, :foreign_key, :association_foreign_key, - :select, :conditions, :include, :order, :group, :having, :limit, :offset, - :uniq, - :finder_sql, :counter_sql, :delete_sql, :insert_sql, - :before_add, :after_add, :before_remove, :after_remove, - :extend, :readonly, - :validate - ] - - def create_has_and_belongs_to_many_reflection(association_id, options, &extension) - options.assert_valid_keys(valid_keys_for_has_and_belongs_to_many_association) - options[:extend] = create_extension_modules(association_id, extension, options[:extend]) - - reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self) - - if reflection.association_foreign_key == reflection.foreign_key - raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection) - end - - reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name)) - if connection.supports_primary_key? && (connection.primary_key(reflection.options[:join_table]) rescue false) - raise HasAndBelongsToManyAssociationWithPrimaryKeyError.new(reflection) - end - - reflection - end - - def add_association_callbacks(association_name, options) - callbacks = %w(before_add after_add before_remove after_remove) - callbacks.each do |callback_name| - full_callback_name = "#{callback_name}_for_#{association_name}" - defined_callbacks = options[callback_name.to_sym] - - full_callback_value = options.has_key?(callback_name.to_sym) ? [defined_callbacks].flatten : [] - - # TODO : why do i need method_defined? I think its because of the inheritance chain - class_attribute full_callback_name.to_sym unless method_defined?(full_callback_name) - self.send("#{full_callback_name}=", full_callback_value) - end - end - - def create_extension_modules(association_id, block_extension, extensions) - if block_extension - extension_module_name = "#{self.to_s.demodulize}#{association_id.to_s.camelize}AssociationExtension" - - silence_warnings do - self.parent.const_set(extension_module_name, Module.new(&block_extension)) - end - Array.wrap(extensions).push("#{self.parent}::#{extension_module_name}".constantize) - else - Array.wrap(extensions) - end - end end end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 2eb431dfec..86904ea2bc 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -19,7 +19,7 @@ module ActiveRecord class Association #:nodoc: attr_reader :owner, :target, :reflection - delegate :options, :klass, :to => :reflection + delegate :options, :to => :reflection def initialize(owner, reflection) reflection.check_validity! @@ -37,13 +37,13 @@ module ActiveRecord # post.comments.aliased_table_name # => "comments" # def aliased_table_name - @reflection.klass.table_name + reflection.klass.table_name end # Resets the \loaded flag to +false+ and sets the \target to +nil+. def reset @loaded = false - IdentityMap.remove(@target) if IdentityMap.enabled? && @target + IdentityMap.remove(target) if IdentityMap.enabled? && target @target = nil end @@ -52,7 +52,7 @@ module ActiveRecord reset construct_scope load_target - self unless @target.nil? + self unless target.nil? end # Has the \target been already \loaded? @@ -93,43 +93,43 @@ module ActiveRecord # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which # actually gets built. def construct_scope - @association_scope = association_scope if target_klass + @association_scope = association_scope if klass end def association_scope - scope = target_klass.unscoped + scope = klass.unscoped scope = scope.create_with(creation_attributes) - scope = scope.apply_finder_options(@reflection.options.slice(:readonly, :include)) - scope = scope.where(interpolate(@reflection.options[:conditions])) + scope = scope.apply_finder_options(options.slice(:readonly, :include)) + scope = scope.where(interpolate(options[:conditions])) if select = select_value scope = scope.select(select) end - scope = scope.extending(*Array.wrap(@reflection.options[:extend])) + scope = scope.extending(*Array.wrap(options[:extend])) scope.where(construct_owner_conditions) end def aliased_table - target_klass.arel_table + klass.arel_table end # Set the inverse association, if possible def set_inverse_instance(record) if record && invertible_for?(record) inverse = record.association(inverse_reflection_for(record).name) - inverse.target = @owner + inverse.target = owner end end # This class of the target. belongs_to polymorphic overrides this to look at the # polymorphic_type field on the owner. - def target_klass - @reflection.klass + def klass + reflection.klass end # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the # through association's scope) def target_scope - target_klass.scoped + klass.scoped end # Loads the \target if needed and returns it. @@ -146,7 +146,7 @@ module ActiveRecord if find_target? begin if IdentityMap.enabled? && association_class && association_class.respond_to?(:base_class) - @target = IdentityMap.get(association_class, @owner[@reflection.foreign_key]) + @target = IdentityMap.get(association_class, owner[reflection.foreign_key]) end rescue NameError nil @@ -163,19 +163,19 @@ module ActiveRecord private def find_target? - !loaded? && (!@owner.new_record? || foreign_key_present?) && target_klass + !loaded? && (!owner.new_record? || foreign_key_present?) && klass end def interpolate(sql, record = nil) if sql.respond_to?(:to_proc) - @owner.send(:instance_exec, record, &sql) + owner.send(:instance_exec, record, &sql) else sql end end def select_value - @reflection.options[:select] + options[:select] end # Implemented by (some) subclasses @@ -184,22 +184,22 @@ module ActiveRecord end # Returns a hash linking the owner to the association represented by the reflection - def construct_owner_attributes(reflection = @reflection) + def construct_owner_attributes(reflection = reflection) attributes = {} if reflection.macro == :belongs_to - attributes[reflection.association_primary_key] = @owner[reflection.foreign_key] + attributes[reflection.association_primary_key] = owner[reflection.foreign_key] else - attributes[reflection.foreign_key] = @owner[reflection.active_record_primary_key] + attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key] - if reflection.options[:as] - attributes["#{reflection.options[:as]}_type"] = @owner.class.base_class.name + if options[:as] + attributes["#{options[:as]}_type"] = owner.class.base_class.name end end attributes end # Builds an array of arel nodes from the owner attributes hash - def construct_owner_conditions(table = aliased_table, reflection = @reflection) + def construct_owner_conditions(table = aliased_table, reflection = reflection) conditions = construct_owner_attributes(reflection).map do |attr, value| table[attr].eq(value) end @@ -208,14 +208,14 @@ module ActiveRecord # Sets the owner attributes on the given record def set_owner_attributes(record) - if @owner.persisted? + if owner.persisted? construct_owner_attributes.each { |key, value| record[key] = value } end end - # Should be true if there is a foreign key present on the @owner which + # Should be true if there is a foreign key present on the owner which # references the target. This is used to determine whether we can load - # the target if the @owner is currently a new record (and therefore + # the target if the owner is currently a new record (and therefore # without a key). # # Currently implemented by belongs_to (vanilla and polymorphic) and @@ -228,8 +228,8 @@ module ActiveRecord # the kind of the class of the associated objects. Meant to be used as # a sanity check when you are about to assign an associated record. def raise_on_type_mismatch(record) - unless record.is_a?(@reflection.klass) || record.is_a?(@reflection.class_name.constantize) - message = "#{@reflection.class_name}(##{@reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})" + unless record.is_a?(reflection.klass) || record.is_a?(reflection.class_name.constantize) + message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})" raise ActiveRecord::AssociationTypeMismatch, message end end @@ -238,7 +238,7 @@ module ActiveRecord # The record parameter is necessary to support polymorphic inverses as we must check for # the association in the specific class of the record. def inverse_reflection_for(record) - @reflection.inverse_of + reflection.inverse_of end # Is this association invertible? Can be redefined by subclasses. diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 178c7204f8..c263edd2c6 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -3,7 +3,7 @@ module ActiveRecord module Associations class BelongsToAssociation < SingularAssociation #:nodoc: def replace(record) - record = check_record(record) + raise_on_type_mismatch(record) if record update_counters(record) replace_keys(record) @@ -21,31 +21,31 @@ module ActiveRecord private def update_counters(record) - counter_cache_name = @reflection.counter_cache_column + counter_cache_name = reflection.counter_cache_column - if counter_cache_name && @owner.persisted? && different_target?(record) + if counter_cache_name && owner.persisted? && different_target?(record) if record record.class.increment_counter(counter_cache_name, record.id) end if foreign_key_present? - target_klass.decrement_counter(counter_cache_name, target_id) + klass.decrement_counter(counter_cache_name, target_id) end end end # Checks whether record is different to the current target, without loading it def different_target?(record) - record.nil? && @owner[@reflection.foreign_key] || - record.id != @owner[@reflection.foreign_key] + record.nil? && owner[reflection.foreign_key] || + record.id != owner[reflection.foreign_key] end def replace_keys(record) - @owner[@reflection.foreign_key] = record && record[@reflection.association_primary_key] + owner[reflection.foreign_key] = record && record[reflection.association_primary_key] end def foreign_key_present? - @owner[@reflection.foreign_key] + owner[reflection.foreign_key] end # NOTE - for now, we're only supporting inverse setting from belongs_to back onto @@ -56,15 +56,15 @@ module ActiveRecord end def target_id - if @reflection.options[:primary_key] - @owner.send(@reflection.name).try(:id) + if options[:primary_key] + owner.send(reflection.name).try(:id) else - @owner[@reflection.foreign_key] + owner[reflection.foreign_key] end end def stale_state - @owner[@reflection.foreign_key].to_s + owner[reflection.foreign_key].to_s 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 4f67b02d00..1ca448236e 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -6,19 +6,19 @@ module ActiveRecord def replace_keys(record) super - @owner[@reflection.foreign_type] = record && record.class.base_class.name + owner[reflection.foreign_type] = record && record.class.base_class.name end def different_target?(record) - super || record.class != target_klass + super || record.class != klass end def inverse_reflection_for(record) - @reflection.polymorphic_inverse_of(record.class) + reflection.polymorphic_inverse_of(record.class) end - def target_klass - type = @owner[@reflection.foreign_type] + def klass + type = owner[reflection.foreign_type] type && type.constantize end @@ -27,7 +27,7 @@ module ActiveRecord end def stale_state - [super, @owner[@reflection.foreign_type].to_s] + [super, owner[reflection.foreign_type].to_s] end end end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb new file mode 100644 index 0000000000..96fca97440 --- /dev/null +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -0,0 +1,53 @@ +module ActiveRecord::Associations::Builder + class Association #:nodoc: + class_attribute :valid_options + self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :extend, :readonly, :validate] + + # Set by subclasses + class_attribute :macro + + attr_reader :model, :name, :options, :reflection + + def self.build(model, name, options) + new(model, name, options).build + end + + def initialize(model, name, options) + @model, @name, @options = model, name, options + end + + def build + validate_options + reflection = model.create_reflection(self.class.macro, name, options, model) + define_accessors + reflection + end + + private + + def validate_options + options.assert_valid_keys(self.class.valid_options) + end + + def define_accessors + define_readers + define_writers + end + + def define_readers + name = self.name + + model.redefine_method(name) do |*params| + association(name).reader(*params) + end + end + + def define_writers + name = self.name + + model.redefine_method("#{name}=") do |value| + association(name).writer(value) + end + end + end +end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb new file mode 100644 index 0000000000..964e7fddc8 --- /dev/null +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -0,0 +1,83 @@ +module ActiveRecord::Associations::Builder + class BelongsTo < SingularAssociation #:nodoc: + self.macro = :belongs_to + + self.valid_options += [:foreign_type, :polymorphic, :touch] + + def constructable? + !options[:polymorphic] + end + + def build + reflection = super + add_counter_cache_callbacks(reflection) if options[:counter_cache] + add_touch_callbacks(reflection) if options[:touch] + configure_dependency + reflection + end + + private + + def add_counter_cache_callbacks(reflection) + cache_column = reflection.counter_cache_column + name = self.name + + method_name = "belongs_to_counter_cache_after_create_for_#{name}" + model.redefine_method(method_name) do + record = send(name) + record.class.increment_counter(cache_column, record.id) unless record.nil? + end + model.after_create(method_name) + + method_name = "belongs_to_counter_cache_before_destroy_for_#{name}" + model.redefine_method(method_name) do + record = send(name) + record.class.decrement_counter(cache_column, record.id) unless record.nil? + end + model.before_destroy(method_name) + + model.send(:module_eval, + "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)", __FILE__, __LINE__ + ) + end + + def add_touch_callbacks(reflection) + name = self.name + method_name = "belongs_to_touch_after_save_or_destroy_for_#{name}" + touch = options[:touch] + + model.redefine_method(method_name) do + record = send(name) + + unless record.nil? + if touch == true + record.touch + else + record.touch(touch) + end + end + end + + model.after_save(method_name) + model.after_touch(method_name) + model.after_destroy(method_name) + end + + def configure_dependency + if options[:dependent] + unless [:destroy, :delete].include?(options[:dependent]) + raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{options[:dependent].inspect})" + end + + method_name = "belongs_to_dependent_#{options[:dependent]}_for_#{name}" + model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1) + def #{method_name} + association = #{name} + association.#{options[:dependent]} if association + end + eoruby + model.after_destroy method_name + end + end + end +end diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb new file mode 100644 index 0000000000..f62209a226 --- /dev/null +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -0,0 +1,75 @@ +module ActiveRecord::Associations::Builder + class CollectionAssociation < Association #:nodoc: + CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] + + self.valid_options += [ + :table_name, :order, :group, :having, :limit, :offset, :uniq, :finder_sql, + :counter_sql, :before_add, :after_add, :before_remove, :after_remove + ] + + attr_reader :block_extension + + def self.build(model, name, options, &extension) + new(model, name, options, &extension).build + end + + def initialize(model, name, options, &extension) + super(model, name, options) + @block_extension = extension + end + + def build + wrap_block_extension + reflection = super + CALLBACKS.each { |callback_name| define_callback(callback_name) } + reflection + end + + def writable? + true + end + + private + + def wrap_block_extension + options[:extend] = Array.wrap(options[:extend]) + + if block_extension + silence_warnings do + model.parent.const_set(extension_module_name, Module.new(&block_extension)) + end + options[:extend].push("#{model.parent}::#{extension_module_name}".constantize) + end + end + + def extension_module_name + @extension_module_name ||= "#{model.to_s.demodulize}#{name.to_s.camelize}AssociationExtension" + end + + def define_callback(callback_name) + full_callback_name = "#{callback_name}_for_#{name}" + + # TODO : why do i need method_defined? I think its because of the inheritance chain + model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name) + model.send("#{full_callback_name}=", Array.wrap(options[callback_name.to_sym])) + end + + def define_readers + super + + name = self.name + model.redefine_method("#{name.to_s.singularize}_ids") do + association(name).ids_reader + end + end + + def define_writers + super + + name = self.name + model.redefine_method("#{name.to_s.singularize}_ids=") do |ids| + association(name).ids_writer(ids) + end + end + end +end diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb new file mode 100644 index 0000000000..e40b32826a --- /dev/null +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -0,0 +1,63 @@ +module ActiveRecord::Associations::Builder + class HasAndBelongsToMany < CollectionAssociation #:nodoc: + self.macro = :has_and_belongs_to_many + + self.valid_options += [:join_table, :association_foreign_key, :delete_sql, :insert_sql] + + def build + reflection = super + check_validity(reflection) + redefine_destroy + reflection + end + + private + + def redefine_destroy + # Don't use a before_destroy callback since users' before_destroy + # callbacks will be executed after the association is wiped out. + name = self.name + model.send(:include, Module.new { + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def destroy # def destroy + super # super + #{name}.clear # posts.clear + end # end + RUBY + }) + end + + # TODO: These checks should probably be moved into the Reflection, and we should not be + # redefining the options[:join_table] value - instead we should define a + # reflection.join_table method. + def check_validity(reflection) + if reflection.association_foreign_key == reflection.foreign_key + raise ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection) + end + + reflection.options[:join_table] ||= join_table_name( + model.send(:undecorated_table_name, model.to_s), + model.send(:undecorated_table_name, reflection.class_name) + ) + + if model.connection.supports_primary_key? && (model.connection.primary_key(reflection.options[:join_table]) rescue false) + raise ActiveRecord::HasAndBelongsToManyAssociationWithPrimaryKeyError.new(reflection) + end + end + + # Generates a join table name from two provided table names. + # The names in the join table names end up in lexicographic order. + # + # join_table_name("members", "clubs") # => "clubs_members" + # join_table_name("members", "special_clubs") # => "members_special_clubs" + def join_table_name(first_table_name, second_table_name) + if first_table_name < second_table_name + join_table = "#{first_table_name}_#{second_table_name}" + else + join_table = "#{second_table_name}_#{first_table_name}" + end + + model.table_name_prefix + join_table + model.table_name_suffix + end + end +end diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb new file mode 100644 index 0000000000..77bb66228d --- /dev/null +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -0,0 +1,63 @@ +module ActiveRecord::Associations::Builder + class HasMany < CollectionAssociation #:nodoc: + self.macro = :has_many + + self.valid_options += [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of] + + def build + reflection = super + configure_dependency + reflection + end + + private + + def configure_dependency + if options[:dependent] + unless [:destroy, :delete_all, :nullify, :restrict].include?(options[:dependent]) + raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, " \ + ":nullify or :restrict (#{options[:dependent].inspect})" + end + + send("define_#{options[:dependent]}_dependency_method") + model.before_destroy dependency_method_name + end + end + + def define_destroy_dependency_method + name = self.name + model.send(:define_method, dependency_method_name) do + send(name).each do |o| + # No point in executing the counter update since we're going to destroy the parent anyway + counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym + if o.respond_to?(counter_method) + class << o + self + end.send(:define_method, counter_method, Proc.new {}) + end + end + + send(name).delete_all + end + end + + def define_delete_all_dependency_method + name = self.name + model.send(:define_method, dependency_method_name) do + send(name).delete_all + end + end + alias :define_nullify_dependency_method :define_delete_all_dependency_method + + def define_restrict_dependency_method + name = self.name + model.send(:define_method, dependency_method_name) do + raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).empty? + end + end + + def dependency_method_name + "has_many_dependent_for_#{name}" + end + end +end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb new file mode 100644 index 0000000000..07ba5d088e --- /dev/null +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -0,0 +1,61 @@ +module ActiveRecord::Associations::Builder + class HasOne < SingularAssociation #:nodoc: + self.macro = :has_one + + self.valid_options += [:order, :as] + + class_attribute :through_options + self.through_options = [:through, :source, :source_type] + + def constructable? + !options[:through] + end + + def build + reflection = super + configure_dependency unless options[:through] + reflection + end + + private + + def validate_options + valid_options = self.class.valid_options + valid_options += self.class.through_options if options[:through] + options.assert_valid_keys(valid_options) + end + + def configure_dependency + if options[:dependent] + unless [:destroy, :delete, :nullify, :restrict].include?(options[:dependent]) + raise ArgumentError, "The :dependent option expects either :destroy, :delete, " \ + ":nullify or :restrict (#{options[:dependent].inspect})" + end + + send("define_#{options[:dependent]}_dependency_method") + model.before_destroy dependency_method_name + end + end + + def dependency_method_name + "has_one_dependent_#{options[:dependent]}_for_#{name}" + end + + def define_destroy_dependency_method + model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1) + def #{dependency_method_name} + association(#{name.to_sym.inspect}).delete + end + eoruby + end + alias :define_delete_dependency_method :define_destroy_dependency_method + alias :define_nullify_dependency_method :define_destroy_dependency_method + + def define_restrict_dependency_method + name = self.name + model.redefine_method(dependency_method_name) do + raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).nil? + end + end + end +end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb new file mode 100644 index 0000000000..06a414b874 --- /dev/null +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -0,0 +1,32 @@ +module ActiveRecord::Associations::Builder + class SingularAssociation < Association #:nodoc: + self.valid_options += [:remote, :dependent, :counter_cache, :primary_key, :inverse_of] + + def constructable? + true + end + + def define_accessors + super + define_constructors if constructable? + end + + private + + def define_constructors + name = self.name + + model.redefine_method("build_#{name}") do |*params| + association(name).build(*params) + end + + model.redefine_method("create_#{name}") do |*params| + association(name).create(*params) + end + + model.redefine_method("create_#{name}!") do |*params| + association(name).create!(*params) + end + end + end +end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 68631681e4..f3761bd2c7 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -32,6 +32,45 @@ module ActiveRecord @proxy = CollectionProxy.new(self) end + # Implements the reader method, e.g. foo.items for Foo.has_many :items + def reader(force_reload = false) + if force_reload + klass.uncached { reload } + elsif stale_target? + reload + end + + proxy + end + + # Implements the writer method, e.g. foo.items= for Foo.has_many :items + def writer(records) + replace(records) + end + + # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items + def ids_reader + if loaded? || options[:finder_sql] + load_target.map do |record| + record.send(reflection.association_primary_key) + end + else + column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" + + scoped.select(column).except(:includes).map! do |record| + record.send(reflection.association_primary_key) + end + end + end + + # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items + def ids_writer(ids) + pk_column = reflection.primary_key_column + ids = Array.wrap(ids).reject { |id| id.blank? } + ids.map! { |i| pk_column.type_cast(i) } + replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids)) + end + def reset @loaded = false @target = [] @@ -47,7 +86,7 @@ module ActiveRecord end def find(*args) - if @reflection.options[:finder_sql] + if options[:finder_sql] find_by_scan(*args) else scoped.find(*args) @@ -67,7 +106,7 @@ module ActiveRecord end def create(attributes = {}, &block) - unless @owner.persisted? + unless owner.persisted? raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" end @@ -84,13 +123,13 @@ module ActiveRecord # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically. def concat(*records) result = true - load_target if @owner.new_record? + load_target if owner.new_record? transaction do records.flatten.each do |record| raise_on_type_mismatch(record) add_to_target(record) do |r| - result &&= insert_record(record) unless @owner.new_record? + result &&= insert_record(record) unless owner.new_record? end end end @@ -108,7 +147,7 @@ module ActiveRecord # # same effect as calling Book.transaction # end def transaction(*args) - @reflection.klass.transaction(*args) do + reflection.klass.transaction(*args) do yield end end @@ -145,26 +184,26 @@ module ActiveRecord # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the # association, it will be used for the query. Otherwise, construct options and pass them with # scope to the target class's +count+. - def count(column_name = nil, options = {}) - column_name, options = nil, column_name if column_name.is_a?(Hash) + def count(column_name = nil, count_options = {}) + column_name, count_options = nil, column_name if column_name.is_a?(Hash) - if @reflection.options[:counter_sql] || @reflection.options[:finder_sql] - unless options.blank? + if options[:counter_sql] || options[:finder_sql] + unless count_options.blank? raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed" end - @reflection.klass.count_by_sql(custom_counter_sql) + reflection.klass.count_by_sql(custom_counter_sql) else - if @reflection.options[:uniq] + if options[:uniq] # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. - column_name ||= @reflection.klass.primary_key - options.merge!(:distinct => true) + column_name ||= reflection.klass.primary_key + count_options.merge!(:distinct => true) end - value = scoped.count(column_name, options) + value = scoped.count(column_name, count_options) - limit = @reflection.options[:limit] - offset = @reflection.options[:offset] + limit = options[:limit] + offset = options[:offset] if limit || offset [ [value - offset.to_i, 0].max, limit.to_i ].min @@ -182,7 +221,7 @@ module ActiveRecord # are actually removed from the database, that depends precisely on # +delete_records+. They are in any case removed from the collection. def delete(*records) - delete_or_destroy(records, @reflection.options[:dependent]) + delete_or_destroy(records, options[:dependent]) end # Destroy +records+ and remove them from this association calling @@ -206,12 +245,12 @@ module ActiveRecord # This method is abstract in the sense that it relies on # +count_records+, which is a method descendants have to provide. def size - if @owner.new_record? || (loaded? && !@reflection.options[:uniq]) - @target.size - elsif !loaded? && @reflection.options[:group] + if owner.new_record? || (loaded? && !options[:uniq]) + target.size + elsif !loaded? && options[:group] load_target.size - elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array) - unsaved_records = @target.select { |r| r.new_record? } + elsif !loaded? && !options[:uniq] && target.is_a?(Array) + unsaved_records = target.select { |r| r.new_record? } unsaved_records.size + count_records else count_records @@ -265,23 +304,23 @@ module ActiveRecord original_target = load_target.dup transaction do - delete(@target - other_array) + delete(target - other_array) - unless concat(other_array - @target) + unless concat(other_array - target) @target = original_target - raise RecordNotSaved, "Failed to replace #{@reflection.name} because one or more of the " \ + raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \ "new records could not be saved." end end end def include?(record) - if record.is_a?(@reflection.klass) + if record.is_a?(reflection.klass) if record.new_record? include_in_memory?(record) else - load_target if @reflection.options[:finder_sql] - loaded? ? @target.include?(record) : scoped.exists?(record) + load_target if options[:finder_sql] + loaded? ? target.include?(record) : scoped.exists?(record) end else false @@ -293,7 +332,7 @@ module ActiveRecord end def association_scope - options = @reflection.options.slice(:order, :limit, :joins, :group, :having, :offset) + options = reflection.options.slice(:order, :limit, :joins, :group, :having, :offset) super.apply_finder_options(options) end @@ -307,7 +346,7 @@ module ActiveRecord reset end - @target = merge_target_lists(targets, @target) + @target = merge_target_lists(targets, target) end loaded! @@ -319,7 +358,7 @@ module ActiveRecord callback(:before_add, record) yield(record) if block_given? - if @reflection.options[:uniq] && index = @target.index(record) + if options[:uniq] && index = @target.index(record) @target[index] = record else @target << record @@ -339,31 +378,31 @@ module ActiveRecord end def uniq_select_value - @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*" + options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*" end def custom_counter_sql - if @reflection.options[:counter_sql] - interpolate(@reflection.options[:counter_sql]) + if options[:counter_sql] + interpolate(options[:counter_sql]) else # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ - interpolate(@reflection.options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } + interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } end end def custom_finder_sql - interpolate(@reflection.options[:finder_sql]) + interpolate(options[:finder_sql]) end def find_target records = - if @reflection.options[:finder_sql] - @reflection.klass.find_by_sql(custom_finder_sql) + if options[:finder_sql] + reflection.klass.find_by_sql(custom_finder_sql) else find(:all) end - records = @reflection.options[:uniq] ? uniq(records) : records + records = options[:uniq] ? uniq(records) : records records.each { |record| set_inverse_instance(record) } records end @@ -408,7 +447,7 @@ module ActiveRecord end def build_record(attributes) - @reflection.build_association(scoped.scope_for_create.merge(attributes)) + reflection.build_association(scoped.scope_for_create.merge(attributes)) end def delete_or_destroy(records, method) @@ -420,7 +459,7 @@ module ActiveRecord records.each { |record| callback(:before_remove, record) } delete_records(existing_records, method) if existing_records.any? - records.each { |record| @target.delete(record) } + records.each { |record| target.delete(record) } records.each { |record| callback(:after_remove, record) } end @@ -436,18 +475,18 @@ module ActiveRecord callbacks_for(method).each do |callback| case callback when Symbol - @owner.send(callback, record) + owner.send(callback, record) when Proc - callback.call(@owner, record) + callback.call(owner, record) else - callback.send(method, @owner, record) + callback.send(method, owner, record) end end end def callbacks_for(callback_name) - full_callback_name = "#{callback_name}_for_#{@reflection.name}" - @owner.class.send(full_callback_name.to_sym) || [] + full_callback_name = "#{callback_name}_for_#{reflection.name}" + owner.class.send(full_callback_name.to_sym) || [] end # Should we deal with assoc.first or assoc.last by issuing an independent query to @@ -466,21 +505,21 @@ module ActiveRecord true else !(loaded? || - @owner.new_record? || - @reflection.options[:finder_sql] || - @target.any? { |record| record.new_record? || record.changed? } || + owner.new_record? || + options[:finder_sql] || + target.any? { |record| record.new_record? || record.changed? } || args.first.kind_of?(Integer)) end end def include_in_memory?(record) - if @reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) - @owner.send(@reflection.through_reflection.name).any? { |source| - target = source.send(@reflection.source_reflection.name) + if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) + owner.send(reflection.through_reflection.name).any? { |source| + target = source.send(reflection.source_reflection.name) target.respond_to?(:include?) ? target.include?(record) : target == record - } || @target.include?(record) + } || target.include?(record) else - @target.include?(record) + target.include?(record) 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 bcaea5ded4..028630977d 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 @@ -12,15 +12,15 @@ module ActiveRecord def insert_record(record, validate = true) return if record.new_record? && !record.save(:validate => validate) - if @reflection.options[:insert_sql] - @owner.connection.insert(interpolate(@reflection.options[:insert_sql], record)) + if options[:insert_sql] + owner.connection.insert(interpolate(options[:insert_sql], record)) else stmt = join_table.compile_insert( - join_table[@reflection.foreign_key] => @owner.id, - join_table[@reflection.association_foreign_key] => record.id + join_table[reflection.foreign_key] => owner.id, + join_table[reflection.association_foreign_key] => record.id ) - @owner.connection.insert stmt.to_sql + owner.connection.insert stmt.to_sql end record @@ -37,23 +37,23 @@ module ActiveRecord end def delete_records(records, method) - if sql = @reflection.options[:delete_sql] - records.each { |record| @owner.connection.delete(interpolate(sql, record)) } + if sql = options[:delete_sql] + records.each { |record| owner.connection.delete(interpolate(sql, record)) } else relation = join_table - stmt = relation.where(relation[@reflection.foreign_key].eq(@owner.id). - and(relation[@reflection.association_foreign_key].in(records.map { |x| x.id }.compact)) + stmt = relation.where(relation[reflection.foreign_key].eq(owner.id). + and(relation[reflection.association_foreign_key].in(records.map { |x| x.id }.compact)) ).compile_delete - @owner.connection.delete stmt.to_sql + owner.connection.delete stmt.to_sql end end def construct_joins right = join_table - left = @reflection.klass.arel_table + left = reflection.klass.arel_table - condition = left[@reflection.klass.primary_key].eq( - right[@reflection.association_foreign_key]) + condition = left[reflection.klass.primary_key].eq( + right[reflection.association_foreign_key]) right.create_join(right, right.create_on(condition)) end @@ -63,7 +63,7 @@ module ActiveRecord end def select_value - super || @reflection.klass.arel_table[Arel.star] + super || reflection.klass.arel_table[Arel.star] end def invertible_for?(record) diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 91565b247a..cebf3e477a 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -6,6 +6,7 @@ module ActiveRecord # If the association has a <tt>:through</tt> option further specialization # is provided by its child HasManyThroughAssociation. class HasManyAssociation < CollectionAssociation #:nodoc: + def insert_record(record, validate = true) set_owner_attributes(record) record.save(:validate => validate) @@ -28,9 +29,9 @@ module ActiveRecord # the loaded flag is set to true as well. def count_records count = if has_cached_counter? - @owner.send(:read_attribute, cached_counter_attribute_name) - elsif @reflection.options[:counter_sql] || @reflection.options[:finder_sql] - @reflection.klass.count_by_sql(custom_counter_sql) + owner.send(:read_attribute, cached_counter_attribute_name) + elsif options[:counter_sql] || options[:finder_sql] + reflection.klass.count_by_sql(custom_counter_sql) else scoped.count end @@ -40,23 +41,23 @@ module ActiveRecord # documented side-effect of the method that may avoid an extra SELECT. @target ||= [] and loaded! if count == 0 - [@reflection.options[:limit], count].compact.min + [options[:limit], count].compact.min end - def has_cached_counter?(reflection = @reflection) - @owner.attribute_present?(cached_counter_attribute_name(reflection)) + def has_cached_counter?(reflection = reflection) + owner.attribute_present?(cached_counter_attribute_name(reflection)) end - def cached_counter_attribute_name(reflection = @reflection) + def cached_counter_attribute_name(reflection = reflection) "#{reflection.name}_count" end - def update_counter(difference, reflection = @reflection) + def update_counter(difference, reflection = reflection) if has_cached_counter?(reflection) counter = cached_counter_attribute_name(reflection) - @owner.class.update_counters(@owner.id, counter => difference) - @owner[counter] += difference - @owner.changed_attributes.delete(counter) # eww + owner.class.update_counters(owner.id, counter => difference) + owner[counter] += difference + owner.changed_attributes.delete(counter) # eww end end @@ -64,13 +65,13 @@ module ActiveRecord # # * An associated record is deleted via record.destroy # * Hence the callbacks run, and they find a belongs_to on the record with a - # :counter_cache options which points back at our @owner. So they update the + # :counter_cache options which points back at our owner. So they update the # counter cache. # * In which case, we must make sure to *not* update the counter cache, or else # it will be decremented twice. # # Hence this method. - def inverse_updates_counter_cache?(reflection = @reflection) + def inverse_updates_counter_cache?(reflection = reflection) counter_name = cached_counter_attribute_name(reflection) reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection| inverse_reflection.counter_cache_column == counter_name @@ -83,13 +84,13 @@ module ActiveRecord records.each { |r| r.destroy } update_counter(-records.length) unless inverse_updates_counter_cache? else - keys = records.map { |r| r[@reflection.association_primary_key] } - scope = scoped.where(@reflection.association_primary_key => keys) + keys = records.map { |r| r[reflection.association_primary_key] } + scope = scoped.where(reflection.association_primary_key => keys) if method == :delete_all update_counter(-scope.delete_all) else - update_counter(-scope.update_all(@reflection.foreign_key => nil)) + update_counter(-scope.update_all(reflection.foreign_key => nil)) end end 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 index 664c284d45..acac68fda5 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -14,16 +14,16 @@ module ActiveRecord # SELECT query if you use #length. def size if has_cached_counter? - @owner.send(:read_attribute, cached_counter_attribute_name) + owner.send(:read_attribute, cached_counter_attribute_name) elsif loaded? - @target.size + target.size else count end end def concat(*records) - unless @owner.new_record? + unless owner.new_record? records.flatten.each do |record| raise_on_type_mismatch(record) record.save! if record.new_record? @@ -43,7 +43,7 @@ module ActiveRecord private def through_record(record) - through_association = @owner.association(@reflection.through_reflection.name) + through_association = owner.association(through_reflection.name) attributes = construct_join_attributes(record) through_record = Array.wrap(through_association.target).find { |candidate| @@ -52,7 +52,7 @@ module ActiveRecord unless through_record through_record = through_association.build(attributes) - through_record.send("#{@reflection.source_reflection.name}=", record) + through_record.send("#{source_reflection.name}=", record) end through_record @@ -61,7 +61,7 @@ module ActiveRecord def build_record(attributes) record = super(attributes) - inverse = @reflection.source_reflection.inverse_of + inverse = source_reflection.inverse_of if inverse if inverse.macro == :has_many record.send(inverse.name) << through_record(record) @@ -74,7 +74,7 @@ module ActiveRecord end def target_reflection_has_associated_record? - if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.foreign_key].blank? + if through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank? false else true @@ -84,7 +84,7 @@ module ActiveRecord def update_through_counter?(method) case method when :destroy - !inverse_updates_counter_cache?(@reflection.through_reflection) + !inverse_updates_counter_cache?(through_reflection) when :nullify false else @@ -93,29 +93,29 @@ module ActiveRecord end def delete_records(records, method) - through = @owner.association(@reflection.through_reflection.name) + through = owner.association(through_reflection.name) scope = through.scoped.where(construct_join_attributes(*records)) case method when :destroy count = scope.destroy_all.length when :nullify - count = scope.update_all(@reflection.source_reflection.foreign_key => nil) + count = scope.update_all(source_reflection.foreign_key => nil) else count = scope.delete_all end delete_through_records(through, records) - if @reflection.through_reflection.macro == :has_many && update_through_counter?(method) - update_counter(-count, @reflection.through_reflection) + if through_reflection.macro == :has_many && update_through_counter?(method) + update_counter(-count, through_reflection) end update_counter(-count) end def delete_through_records(through, records) - if @reflection.through_reflection.macro == :has_many + if through_reflection.macro == :has_many records.each do |record| through.target.delete(through_record(record)) end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index a0828dcdea..e13f97125f 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -3,22 +3,22 @@ module ActiveRecord module Associations class HasOneAssociation < SingularAssociation #:nodoc: def replace(record, save = true) - record = check_record(record) + raise_on_type_mismatch(record) if record load_target - @reflection.klass.transaction do - if @target && @target != record - remove_target!(@reflection.options[:dependent]) + reflection.klass.transaction do + if target && target != record + remove_target!(options[:dependent]) end if record set_inverse_instance(record) set_owner_attributes(record) - if @owner.persisted? && save && !record.save + if owner.persisted? && save && !record.save nullify_owner_attributes(record) - set_owner_attributes(@target) - raise RecordNotSaved, "Failed to save the new associated #{@reflection.name}." + set_owner_attributes(target) + raise RecordNotSaved, "Failed to save the new associated #{reflection.name}." end end end @@ -26,11 +26,22 @@ module ActiveRecord self.target = record end - protected - - def association_scope - super.order(@reflection.options[:order]) + def delete(method = options[:dependent]) + if load_target + case method + when :delete + target.delete + when :destroy + target.destroy + when :nullify + target.update_attribute(reflection.foreign_key, nil) + end end + end + + def association_scope + super.order(options[:order]) + end private @@ -46,20 +57,20 @@ module ActiveRecord def remove_target!(method) if [:delete, :destroy].include?(method) - @target.send(method) + target.send(method) else - nullify_owner_attributes(@target) + nullify_owner_attributes(target) - if @target.persisted? && @owner.persisted? && !@target.save - set_owner_attributes(@target) - raise RecordNotSaved, "Failed to remove the existing associated #{@reflection.name}. " + + if target.persisted? && owner.persisted? && !target.save + set_owner_attributes(target) + raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " + "The record failed to save when after its foreign key was set to nil." end end end def nullify_owner_attributes(record) - record[@reflection.foreign_key] = nil + record[reflection.foreign_key] = nil end end end diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index 112b773ec4..d76d729303 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -12,7 +12,7 @@ module ActiveRecord private def create_through_record(record) - through_proxy = @owner.association(@reflection.through_reflection.name) + through_proxy = owner.association(through_reflection.name) through_record = through_proxy.send(:load_target) if through_record && !record @@ -22,7 +22,7 @@ module ActiveRecord if through_record through_record.update_attributes(attributes) - elsif @owner.new_record? + elsif owner.new_record? through_proxy.build(attributes) else through_proxy.create(attributes) diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 0aa647c63d..0d8e45adb5 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -1,6 +1,22 @@ module ActiveRecord module Associations class SingularAssociation < Association #:nodoc: + # Implements the reader method, e.g. foo.bar for Foo.has_one :bar + def reader(force_reload = false) + if force_reload + klass.uncached { reload } + elsif !loaded? || stale_target? + reload + end + + target + end + + # Implements the writer method, e.g. foo.items= for Foo.has_many :items + def writer(record) + replace(record) + end + def create(attributes = {}) new_record(:create, attributes) end @@ -28,15 +44,9 @@ module ActiveRecord replace(record) end - def check_record(record) - record = record.target if Association === record - raise_on_type_mismatch(record) if record - record - end - def new_record(method, attributes) attributes = scoped.scope_for_create.merge(attributes || {}) - record = @reflection.send("#{method}_association", attributes) + record = reflection.send("#{method}_association", attributes) set_new_record(record) record end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 8db8068295..e1d60ccb17 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -3,17 +3,19 @@ module ActiveRecord module Associations module ThroughAssociation #:nodoc: + delegate :source_options, :through_options, :source_reflection, :through_reflection, :to => :reflection + protected def target_scope - super.merge(@reflection.through_reflection.klass.scoped) + super.merge(through_reflection.klass.scoped) end def association_scope scope = super.joins(construct_joins) scope = add_conditions(scope) - unless @reflection.options[:include] - scope = scope.includes(@reflection.source_reflection.options[:include]) + unless options[:include] + scope = scope.includes(source_options[:include]) end scope end @@ -29,40 +31,40 @@ module ActiveRecord end def aliased_through_table - name = @reflection.through_reflection.table_name + name = through_reflection.table_name - @reflection.table_name == name ? - @reflection.through_reflection.klass.arel_table.alias(name + "_join") : - @reflection.through_reflection.klass.arel_table + reflection.table_name == name ? + through_reflection.klass.arel_table.alias(name + "_join") : + through_reflection.klass.arel_table end def construct_owner_conditions - super(aliased_through_table, @reflection.through_reflection) + super(aliased_through_table, through_reflection) end def construct_joins right = aliased_through_table - left = @reflection.klass.arel_table + left = reflection.klass.arel_table conditions = [] - if @reflection.source_reflection.macro == :belongs_to - reflection_primary_key = @reflection.source_reflection.association_primary_key - source_primary_key = @reflection.source_reflection.foreign_key + if source_reflection.macro == :belongs_to + reflection_primary_key = source_reflection.association_primary_key + source_primary_key = source_reflection.foreign_key - if @reflection.options[:source_type] - column = @reflection.source_reflection.foreign_type + if options[:source_type] + column = source_reflection.foreign_type conditions << - right[column].eq(@reflection.options[:source_type]) + right[column].eq(options[:source_type]) end else - reflection_primary_key = @reflection.source_reflection.foreign_key - source_primary_key = @reflection.source_reflection.active_record_primary_key + reflection_primary_key = source_reflection.foreign_key + source_primary_key = source_reflection.active_record_primary_key - if @reflection.source_reflection.options[:as] - column = "#{@reflection.source_reflection.options[:as]}_type" + if source_options[:as] + column = "#{source_options[:as]}_type" conditions << - left[column].eq(@reflection.through_reflection.klass.name) + left[column].eq(through_reflection.klass.name) end end @@ -87,19 +89,19 @@ module ActiveRecord # situation it is more natural for the user to just create or modify their join records # directly as required. def construct_join_attributes(*records) - if @reflection.source_reflection.macro != :belongs_to - raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) + if source_reflection.macro != :belongs_to + raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) end join_attributes = { - @reflection.source_reflection.foreign_key => + source_reflection.foreign_key => records.map { |record| - record.send(@reflection.source_reflection.association_primary_key) + record.send(source_reflection.association_primary_key) } } - if @reflection.options[:source_type] - join_attributes[@reflection.source_reflection.foreign_type] = + if options[:source_type] + join_attributes[source_reflection.foreign_type] = records.map { |record| record.class.base_class.name } end @@ -115,18 +117,18 @@ module ActiveRecord # has a different meaning to scope.where(x).where(y) - the first version might # perform some substitution if x is a string. def add_conditions(scope) - unless @reflection.through_reflection.klass.descends_from_active_record? - scope = scope.where(@reflection.through_reflection.klass.send(:type_condition)) + unless through_reflection.klass.descends_from_active_record? + scope = scope.where(through_reflection.klass.send(:type_condition)) end - scope = scope.where(interpolate(@reflection.source_reflection.options[:conditions])) + scope = scope.where(interpolate(source_options[:conditions])) scope.where(through_conditions) end # If there is a hash of conditions then we make sure the keys are scoped to the # through table name if left ambiguous. def through_conditions - conditions = interpolate(@reflection.through_reflection.options[:conditions]) + conditions = interpolate(through_options[:conditions]) if conditions.is_a?(Hash) Hash[conditions.map { |key, value| @@ -142,14 +144,14 @@ module ActiveRecord end def stale_state - if @reflection.through_reflection.macro == :belongs_to - @owner[@reflection.through_reflection.foreign_key].to_s + if through_reflection.macro == :belongs_to + owner[through_reflection.foreign_key].to_s end end def foreign_key_present? - @reflection.through_reflection.macro == :belongs_to && - !@owner[@reflection.through_reflection.foreign_key].nil? + through_reflection.macro == :belongs_to && + !owner[through_reflection.foreign_key].nil? end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 476598bf88..748cc99a62 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -116,30 +116,29 @@ module ActiveRecord module AutosaveAssociation extend ActiveSupport::Concern - ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many } + ASSOCIATION_TYPES = %w{ HasOne HasMany BelongsTo HasAndBelongsToMany } + + module AssociationBuilderExtension #:nodoc: + def self.included(base) + base.valid_options << :autosave + end + + def build + reflection = super + model.send(:add_autosave_association_callbacks, reflection) + reflection + end + end included do ASSOCIATION_TYPES.each do |type| - send("valid_keys_for_#{type}_association") << :autosave + Associations::Builder.const_get(type).send(:include, AssociationBuilderExtension) end end module ClassMethods private - # def belongs_to(name, options = {}) - # super - # add_autosave_association_callbacks(reflect_on_association(name)) - # end - ASSOCIATION_TYPES.each do |type| - module_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{type}(name, options = {}) - super - add_autosave_association_callbacks(reflect_on_association(name)) - end - CODE - end - def define_non_cyclic_method(name, reflection, &block) define_method(name) do |*args| result = true; @_already_called ||= {} diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 4093a1a209..49659b3aa5 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -394,6 +394,14 @@ module ActiveRecord @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym } end + def source_options + source_reflection.options + end + + def through_options + through_reflection.options + end + def association_primary_key source_reflection.association_primary_key end diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index e1f5b16eca..24830a661a 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -29,7 +29,7 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase assert_equal projects(:action_controller), developers(:david).projects_extended_by_name_and_block.find_most_recent assert_equal projects(:active_record), developers(:david).projects_extended_by_name_and_block.find_least_recent end - + def test_extension_with_scopes assert_equal comments(:greetings), posts(:welcome).comments.offset(1).find_most_recent assert_equal comments(:greetings), posts(:welcome).comments.not_again.find_most_recent @@ -52,12 +52,16 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase end def test_extension_name - extension = Proc.new {} - name = :association_name - - assert_equal 'DeveloperAssociationNameAssociationExtension', Developer.send(:create_extension_modules, name, extension, []).first.name - assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', MyApplication::Business::Developer.send(:create_extension_modules, name, extension, []).first.name - assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', MyApplication::Business::Developer.send(:create_extension_modules, name, extension, []).first.name - assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', MyApplication::Business::Developer.send(:create_extension_modules, name, extension, []).first.name + assert_equal 'DeveloperAssociationNameAssociationExtension', extension_name(Developer) + assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', extension_name(MyApplication::Business::Developer) + assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', extension_name(MyApplication::Business::Developer) end + + private + + def extension_name(model) + builder = ActiveRecord::Associations::Builder::HasMany.new(model, :association_name, {}) { } + builder.send(:wrap_block_extension) + builder.options[:extend].first.name + end end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index dbc5d71153..b9d9d89220 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -21,19 +21,19 @@ require 'models/eye' class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase def test_autosave_should_be_a_valid_option_for_has_one - assert base.valid_keys_for_has_one_association.include?(:autosave) + assert ActiveRecord::Associations::Builder::HasOne.valid_options.include?(:autosave) end def test_autosave_should_be_a_valid_option_for_belongs_to - assert base.valid_keys_for_belongs_to_association.include?(:autosave) + assert ActiveRecord::Associations::Builder::BelongsTo.valid_options.include?(:autosave) end def test_autosave_should_be_a_valid_option_for_has_many - assert base.valid_keys_for_has_many_association.include?(:autosave) + assert ActiveRecord::Associations::Builder::HasMany.valid_options.include?(:autosave) end def test_autosave_should_be_a_valid_option_for_has_and_belongs_to_many - assert base.valid_keys_for_has_and_belongs_to_many_association.include?(:autosave) + assert ActiveRecord::Associations::Builder::HasAndBelongsToMany.valid_options.include?(:autosave) end def test_should_not_add_the_same_callbacks_multiple_times_for_has_one diff --git a/activerecord/test/cases/custom_locking_test.rb b/activerecord/test/cases/custom_locking_test.rb new file mode 100644 index 0000000000..d63ecdbcc5 --- /dev/null +++ b/activerecord/test/cases/custom_locking_test.rb @@ -0,0 +1,17 @@ +require "cases/helper" +require 'models/person' + +module ActiveRecord + class CustomLockingTest < ActiveRecord::TestCase + fixtures :people + + def test_custom_lock + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + assert_match 'SHARE MODE', Person.lock('LOCK IN SHARE MODE').to_sql + assert_sql(/LOCK IN SHARE MODE/) do + Person.find(1, :lock => 'LOCK IN SHARE MODE') + end + end + end + end +end diff --git a/load_paths.rb b/load_paths.rb index 873315f978..8f37364629 100644 --- a/load_paths.rb +++ b/load_paths.rb @@ -4,7 +4,7 @@ rescue LoadError begin # bust gem prelude if defined? Gem - Gem.cache + Gem.source_index gem 'bundler' else require 'rubygems' |