aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--activerecord/lib/active_record/associations.rb409
-rw-r--r--activerecord/lib/active_record/associations/association.rb60
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb24
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb12
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb53
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb83
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb75
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb63
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb63
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb61
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb32
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb147
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb28
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb33
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb26
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb45
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb24
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb70
-rw-r--r--activerecord/lib/active_record/autosave_association.rb29
-rw-r--r--activerecord/lib/active_record/reflection.rb8
-rw-r--r--activerecord/test/cases/associations/extension_test.rb20
-rw-r--r--activerecord/test/cases/autosave_association_test.rb8
-rw-r--r--activerecord/test/cases/custom_locking_test.rb17
-rw-r--r--load_paths.rb2
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'