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

Merge remote branch 'jonleighton/association_fixes' into fuuu

* jonleighton/association_fixes:
  Rename AssociationReflection#primary_key_name to foreign_key, since the options key which it relates to is :foreign_key
  Support for :counter_cache on polymorphic belongs_to
  Refactor BelongsToAssociation to allow BelongsToPolymorphicAssociation to inherit from it
  Specify the STI type condition using SQL IN rather than a whole load of ORs. Required a fix to ActiveRecord::Relation#merge for properly merging create_with_value. This also fixes a situation where the type condition was appearing twice in the resultant SQL query.
  Verify that when has_many associated objects are destroyed via :dependent => :destroy, when the parent is destroyed, the callbacks are run
  Get rid of extra_conditions param from configure_dependency_for_has_many. I can't see a particularly plausible argument for this being used by plugins, and if they really want they can just redefine the callback or whatever. Note also that before my recent commit the extra_conditions param was completely ignored for :dependent => :destroy.
  And owner_quoted_id can go too
  Now we can drop-kick AssociationReflection#dependent_conditions into oblivion.
  Refactor configure_dependency_for_has_many to use AssociationCollection#delete_all. It was necessary to change test_before_destroy in lifecycle_test.rb so that it checks topic.replies.size *before* doing the destroy, as afterwards it will now (correctly) be 0.
This commit is contained in:
Aaron Patterson 2011-01-01 11:43:53 -08:00
commit 5c7fd87665
26 changed files with 261 additions and 256 deletions

View file

@ -205,7 +205,7 @@ module ActiveRecord
# FIXME: options[:select] is always nil in the tests. Do we really # FIXME: options[:select] is always nil in the tests. Do we really
# need it? # need it?
options[:select] || left[Arel.star], options[:select] || left[Arel.star],
right[reflection.primary_key_name].as( right[reflection.foreign_key].as(
Arel.sql('the_parent_record_id')) Arel.sql('the_parent_record_id'))
] ]
@ -220,7 +220,7 @@ module ActiveRecord
all_associated_records = associated_records(ids) do |some_ids| all_associated_records = associated_records(ids) do |some_ids|
method = in_or_equal(some_ids) method = in_or_equal(some_ids)
conditions = right[reflection.primary_key_name].send(*method) conditions = right[reflection.foreign_key].send(*method)
conditions = custom_conditions.inject(conditions) do |ast, cond| conditions = custom_conditions.inject(conditions) do |ast, cond|
ast.and cond ast.and cond
end end
@ -241,7 +241,7 @@ module ActiveRecord
unless through_records.empty? unless through_records.empty?
through_reflection = reflections[options[:through]] through_reflection = reflections[options[:through]]
through_primary_key = through_reflection.primary_key_name through_primary_key = through_reflection.foreign_key
source = reflection.source_reflection.name source = reflection.source_reflection.name
through_records.first.class.preload_associations(through_records, source) through_records.first.class.preload_associations(through_records, source)
if through_reflection.macro == :belongs_to if through_reflection.macro == :belongs_to
@ -255,7 +255,7 @@ module ActiveRecord
end end
end end
else else
set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name) set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.foreign_key)
end end
end end
@ -263,8 +263,8 @@ module ActiveRecord
return if records.first.send(reflection.name).loaded? return if records.first.send(reflection.name).loaded?
options = reflection.options options = reflection.options
primary_key_name = reflection.through_reflection_primary_key_name foreign_key = reflection.through_reflection_foreign_key
id_to_record_map, ids = construct_id_map(records, primary_key_name || reflection.options[:primary_key]) id_to_record_map, ids = construct_id_map(records, foreign_key || reflection.options[:primary_key])
records.each {|record| record.send(reflection.name).loaded} records.each {|record| record.send(reflection.name).loaded}
if options[:through] if options[:through]
@ -281,7 +281,7 @@ module ActiveRecord
else else
set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options),
reflection.primary_key_name) reflection.foreign_key)
end end
end end
@ -319,7 +319,7 @@ module ActiveRecord
def preload_belongs_to_association(records, reflection, preload_options={}) def preload_belongs_to_association(records, reflection, preload_options={})
return if records.first.send("loaded_#{reflection.name}?") return if records.first.send("loaded_#{reflection.name}?")
options = reflection.options options = reflection.options
primary_key_name = reflection.primary_key_name foreign_key = reflection.foreign_key
klasses_and_ids = {} klasses_and_ids = {}
@ -330,7 +330,7 @@ module ActiveRecord
# to their parent_records # to their parent_records
records.each do |record| records.each do |record|
if klass = record.send(polymorph_type) if klass = record.send(polymorph_type)
klass_id = record.send(primary_key_name) klass_id = record.send(foreign_key)
if klass_id if klass_id
id_map = klasses_and_ids[klass.constantize] ||= {} id_map = klasses_and_ids[klass.constantize] ||= {}
(id_map[klass_id.to_s] ||= []) << record (id_map[klass_id.to_s] ||= []) << record
@ -339,7 +339,7 @@ module ActiveRecord
end end
else else
id_map = records.group_by do |record| id_map = records.group_by do |record|
key = record.send(primary_key_name) key = record.send(foreign_key)
key && key.to_s key && key.to_s
end end
id_map.delete nil id_map.delete nil
@ -369,7 +369,7 @@ module ActiveRecord
conditions = [] conditions = []
key = reflection.primary_key_name key = reflection.foreign_key
if interface = reflection.options[:as] if interface = reflection.options[:as]
key = "#{interface}_id" key = "#{interface}_id"

View file

@ -1612,16 +1612,14 @@ module ActiveRecord
# - set the foreign key to NULL if the option is set to :nullify # - 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 # - do not delete the parent record if there is any child record if the
# option is set to :restrict # option is set to :restrict
# def configure_dependency_for_has_many(reflection)
# The +extra_conditions+ parameter, which is not used within the main if reflection.options[:dependent]
# Active Record codebase, is meant to allow plugins to define extra method_name = "has_many_dependent_for_#{reflection.name}"
# finder conditions.
def configure_dependency_for_has_many(reflection, extra_conditions = nil)
if reflection.options.include?(:dependent)
case reflection.options[:dependent] case reflection.options[:dependent]
when :destroy when :destroy, :delete_all, :nullify
method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym define_method(method_name) do
define_method(method_name) do if reflection.options[:dependent] == :destroy
send(reflection.name).each do |o| send(reflection.name).each do |o|
# No point in executing the counter update since we're going to destroy the parent anyway # 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 counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym
@ -1633,35 +1631,21 @@ module ActiveRecord
o.destroy o.destroy
end end
end end
before_destroy method_name
when :delete_all # AssociationProxy#delete_all looks at the :dependent option and acts accordingly
before_destroy do |record| send(reflection.name).delete_all
self.class.send(:delete_all_has_many_dependencies, end
record, when :restrict
reflection.name, define_method(method_name) do
reflection.klass, unless send(reflection.name).empty?
reflection.dependent_conditions(record, self.class, extra_conditions)) raise DeleteRestrictionError.new(reflection)
end end
when :nullify end
before_destroy do |record| else
self.class.send(:nullify_has_many_dependencies, raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, :nullify or :restrict (#{reflection.options[:dependent].inspect})"
record,
reflection.name,
reflection.klass,
reflection.primary_key_name,
reflection.dependent_conditions(record, self.class, extra_conditions))
end
when :restrict
method_name = "has_many_dependent_restrict_for_#{reflection.name}".to_sym
define_method(method_name) do
unless send(reflection.name).empty?
raise DeleteRestrictionError.new(reflection)
end
end
before_destroy method_name
else
raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, :nullify or :restrict (#{reflection.options[:dependent].inspect})"
end end
before_destroy method_name
end end
end end
@ -1686,7 +1670,7 @@ module ActiveRecord
class_eval <<-eoruby, __FILE__, __LINE__ + 1 class_eval <<-eoruby, __FILE__, __LINE__ + 1
def #{method_name} def #{method_name}
association = #{reflection.name} association = #{reflection.name}
association.update_attribute(#{reflection.primary_key_name.inspect}, nil) if association association.update_attribute(#{reflection.foreign_key.inspect}, nil) if association
end end
eoruby eoruby
when :restrict when :restrict
@ -1724,14 +1708,6 @@ module ActiveRecord
end end
end end
def delete_all_has_many_dependencies(record, reflection_name, association_class, dependent_conditions)
association_class.delete_all(dependent_conditions)
end
def nullify_has_many_dependencies(record, reflection_name, association_class, primary_key_name, dependent_conditions)
association_class.update_all("#{primary_key_name} = NULL", dependent_conditions)
end
mattr_accessor :valid_keys_for_has_many_association mattr_accessor :valid_keys_for_has_many_association
@@valid_keys_for_has_many_association = [ @@valid_keys_for_has_many_association = [
:class_name, :table_name, :foreign_key, :primary_key, :class_name, :table_name, :foreign_key, :primary_key,
@ -1806,7 +1782,7 @@ module ActiveRecord
reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self) reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self)
if reflection.association_foreign_key == reflection.primary_key_name if reflection.association_foreign_key == reflection.foreign_key
raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection) raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection)
end end

View file

@ -31,10 +31,6 @@ module ActiveRecord
end end
end end
def scoped
with_scope(@scope) { @reflection.klass.scoped }
end
def find(*args) def find(*args)
options = args.extract_options! options = args.extract_options!
@ -488,7 +484,7 @@ module ActiveRecord
ensure_owner_is_persisted! ensure_owner_is_persisted!
transaction do transaction do
with_scope(:create => @scope[:create].merge(scoped.where_values_hash)) do with_scope(:create => @scope[:create].merge(scoped.scope_for_create)) do
build_record(attrs, &block) build_record(attrs, &block)
end end
end end

View file

@ -8,7 +8,7 @@ module ActiveRecord
# #
# AssociationProxy # AssociationProxy
# BelongsToAssociation # BelongsToAssociation
# BelongsToPolymorphicAssociation # BelongsToPolymorphicAssociation
# AssociationCollection + HasAssociation # AssociationCollection + HasAssociation
# HasAndBelongsToManyAssociation # HasAndBelongsToManyAssociation
# HasManyAssociation # HasManyAssociation
@ -116,6 +116,7 @@ module ActiveRecord
# Reloads the \target and returns +self+ on success. # Reloads the \target and returns +self+ on success.
def reload def reload
reset reset
construct_scope
load_target load_target
self unless @target.nil? self unless @target.nil?
end end
@ -166,6 +167,10 @@ module ActiveRecord
end end
end end
def scoped
with_scope(@scope) { target_klass.scoped }
end
protected protected
def interpolate_sql(sql, record = nil) def interpolate_sql(sql, record = nil)
@owner.send(:interpolate_sql, sql, record) @owner.send(:interpolate_sql, sql, record)
@ -192,15 +197,19 @@ module ActiveRecord
# Forwards +with_scope+ to the reflection. # Forwards +with_scope+ to the reflection.
def with_scope(*args, &block) def with_scope(*args, &block)
@reflection.klass.send :with_scope, *args, &block target_klass.send :with_scope, *args, &block
end end
# Construct the scope used for find/create queries on the target # Construct the scope used for find/create queries on the target
def construct_scope def construct_scope
@scope = { if target_klass
:find => construct_find_scope, @scope = {
:create => construct_create_scope :find => construct_find_scope,
} :create => construct_create_scope
}
else
@scope = nil
end
end end
# Implemented by subclasses # Implemented by subclasses
@ -214,7 +223,7 @@ module ActiveRecord
end end
def aliased_table def aliased_table
@reflection.klass.arel_table target_klass.arel_table
end end
# Set the inverse association, if possible # Set the inverse association, if possible
@ -224,6 +233,12 @@ module ActiveRecord
end end
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
end
private private
# Forwards any missing method call to the \target. # Forwards any missing method call to the \target.
def method_missing(method, *args) def method_missing(method, *args)
@ -254,7 +269,7 @@ module ActiveRecord
def load_target def load_target
return nil unless defined?(@loaded) return nil unless defined?(@loaded)
if !loaded? && (!@owner.new_record? || foreign_key_present) if !loaded? && (!@owner.new_record? || foreign_key_present) && @scope
@target = find_target @target = find_target
end end
@ -282,11 +297,6 @@ module ActiveRecord
end end
end end
# Returns the ID of the owner, quoted if needed.
def owner_quoted_id
@owner.quoted_id
end
# Can be redefined by subclasses, notably polymorphic belongs_to # Can be redefined by subclasses, notably polymorphic belongs_to
# The record parameter is necessary to support polymorphic inverses as we must check for # The record parameter is necessary to support polymorphic inverses as we must check for
# the association in the specific class of the record. # the association in the specific class of the record.

View file

@ -11,29 +11,16 @@ module ActiveRecord
end end
def replace(record) def replace(record)
counter_cache_name = @reflection.counter_cache_column record = record.target if AssociationProxy === record
raise_on_type_mismatch(record) unless record.nil?
if record.nil?
if counter_cache_name && @owner.persisted?
@reflection.klass.decrement_counter(counter_cache_name, previous_record_id) if @owner[@reflection.primary_key_name]
end
@target = @owner[@reflection.primary_key_name] = nil
else
raise_on_type_mismatch(record)
if counter_cache_name && @owner.persisted? && record.id != @owner[@reflection.primary_key_name]
@reflection.klass.increment_counter(counter_cache_name, record.id)
@reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
end
@target = (AssociationProxy === record ? record.target : record)
@owner[@reflection.primary_key_name] = record_id(record) if record.persisted?
@updated = true
end
update_counters(record)
replace_keys(record)
set_inverse_instance(record) set_inverse_instance(record)
@target = record
@updated = true if record
loaded loaded
record record
end end
@ -44,8 +31,8 @@ module ActiveRecord
def stale_target? def stale_target?
if @target && @target.persisted? if @target && @target.persisted?
target_id = @target.send(@reflection.association_primary_key).to_s target_id = @target[@reflection.association_primary_key].to_s
foreign_key = @owner.send(@reflection.primary_key_name).to_s foreign_key = @owner[@reflection.foreign_key].to_s
target_id != foreign_key target_id != foreign_key
else else
@ -54,31 +41,55 @@ module ActiveRecord
end end
private private
def find_target def update_counters(record)
find_method = if @reflection.options[:primary_key] counter_cache_name = @reflection.counter_cache_column
"find_by_#{@reflection.options[:primary_key]}"
else
"find"
end
options = @reflection.options.dup.slice(:select, :include, :readonly) if counter_cache_name && @owner.persisted? && different_target?(record)
if record
record.class.increment_counter(counter_cache_name, record.id)
end
the_target = with_scope(:find => @scope[:find]) do if foreign_key_present
@reflection.klass.send(find_method, target_klass.decrement_counter(counter_cache_name, target_id)
@owner[@reflection.primary_key_name], end
options end
) if @owner[@reflection.primary_key_name] 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]
end
def replace_keys(record)
@owner[@reflection.foreign_key] = record && record[@reflection.association_primary_key]
end
def find_target
if foreign_key_present
scoped.first.tap { |record| set_inverse_instance(record) }
end end
set_inverse_instance(the_target)
the_target
end end
def construct_find_scope def construct_find_scope
{ :conditions => conditions } {
:conditions => construct_conditions,
:select => @reflection.options[:select],
:include => @reflection.options[:include],
:readonly => @reflection.options[:readonly]
}
end
def construct_conditions
conditions = aliased_table[@reflection.association_primary_key].
eq(@owner[@reflection.foreign_key])
conditions = conditions.and(Arel.sql(sql_conditions)) if sql_conditions
conditions
end end
def foreign_key_present def foreign_key_present
!@owner[@reflection.primary_key_name].nil? !@owner[@reflection.foreign_key].nil?
end end
# NOTE - for now, we're only supporting inverse setting from belongs_to back onto # NOTE - for now, we're only supporting inverse setting from belongs_to back onto
@ -88,17 +99,12 @@ module ActiveRecord
inverse && inverse.macro == :has_one inverse && inverse.macro == :has_one
end end
def record_id(record) def target_id
record.send(@reflection.options[:primary_key] || :id) if @reflection.options[:primary_key]
end @owner.send(@reflection.name).try(:id)
else
def previous_record_id @owner[@reflection.foreign_key]
@previous_record_id ||= if @reflection.options[:primary_key] end
previous_record = @owner.send(@reflection.name)
previous_record.nil? ? nil : previous_record.id
else
@owner[@reflection.primary_key_name]
end
end end
end end
end end

View file

@ -1,32 +1,11 @@
module ActiveRecord module ActiveRecord
# = Active Record Belongs To Polymorphic Association # = Active Record Belongs To Polymorphic Association
module Associations module Associations
class BelongsToPolymorphicAssociation < AssociationProxy #:nodoc: class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc:
def replace(record)
if record.nil?
@target = @owner[@reflection.primary_key_name] = @owner[@reflection.options[:foreign_type]] = nil
else
@target = (AssociationProxy === record ? record.target : record)
@owner[@reflection.primary_key_name] = record_id(record)
@owner[@reflection.options[:foreign_type]] = record.class.base_class.name.to_s
@updated = true
end
set_inverse_instance(record)
loaded
record
end
def updated?
@updated
end
def stale_target? def stale_target?
if @target && @target.persisted? if @target && @target.persisted?
target_id = @target.send(@reflection.association_primary_key).to_s target_id = @target.send(@reflection.association_primary_key).to_s
foreign_key = @owner.send(@reflection.primary_key_name).to_s foreign_key = @owner.send(@reflection.foreign_key).to_s
target_type = @target.class.base_class.name target_type = @target.class.base_class.name
foreign_type = @owner.send(@reflection.options[:foreign_type]).to_s foreign_type = @owner.send(@reflection.options[:foreign_type]).to_s
@ -38,43 +17,26 @@ module ActiveRecord
private private
def replace_keys(record)
super
@owner[@reflection.options[:foreign_type]] = record && record.class.base_class.name
end
def different_target?(record)
super || record.class != target_klass
end
def inverse_reflection_for(record) def inverse_reflection_for(record)
@reflection.polymorphic_inverse_of(record.class) @reflection.polymorphic_inverse_of(record.class)
end end
def invertible_for?(record) def target_klass
inverse = inverse_reflection_for(record) type = @owner[@reflection.options[:foreign_type]]
inverse && inverse.macro == :has_one type && type.constantize
end end
def construct_find_scope def raise_on_type_mismatch(record)
{ :conditions => conditions } # A polymorphic association cannot have a type mismatch, by definition
end
def find_target
return nil if association_class.nil?
target = association_class.send(:with_scope, :find => @scope[:find]) do
association_class.find(
@owner[@reflection.primary_key_name],
:select => @reflection.options[:select],
:include => @reflection.options[:include]
)
end
set_inverse_instance(target)
target
end
def foreign_key_present
!@owner[@reflection.primary_key_name].nil?
end
def record_id(record)
record.send(@reflection.options[:primary_key] || :id)
end
def association_class
@owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil
end end
end end
end end

View file

@ -193,11 +193,11 @@ module ActiveRecord
first_key = second_key = nil first_key = second_key = nil
if through_reflection.macro == :belongs_to if through_reflection.macro == :belongs_to
jt_primary_key = through_reflection.primary_key_name jt_primary_key = through_reflection.foreign_key
jt_foreign_key = through_reflection.association_primary_key jt_foreign_key = through_reflection.association_primary_key
else else
jt_primary_key = through_reflection.active_record_primary_key jt_primary_key = through_reflection.active_record_primary_key
jt_foreign_key = through_reflection.primary_key_name jt_foreign_key = through_reflection.foreign_key
if through_reflection.options[:as] # has_many :through against a polymorphic join if through_reflection.options[:as] # has_many :through against a polymorphic join
jt_conditions << jt_conditions <<
@ -231,7 +231,7 @@ module ActiveRecord
join_table[reflection.source_reflection.options[:foreign_type]]. join_table[reflection.source_reflection.options[:foreign_type]].
eq(reflection.options[:source_type]) eq(reflection.options[:source_type])
else else
second_key = source_reflection.primary_key_name second_key = source_reflection.foreign_key
end end
end end
@ -262,7 +262,7 @@ module ActiveRecord
end end
def join_belongs_to_to(relation) def join_belongs_to_to(relation)
foreign_key = options[:foreign_key] || reflection.primary_key_name foreign_key = options[:foreign_key] || reflection.foreign_key
primary_key = options[:primary_key] || reflection.klass.primary_key primary_key = options[:primary_key] || reflection.klass.primary_key
join_target_table( join_target_table(

View file

@ -40,7 +40,7 @@ module ActiveRecord
attributes = columns.map do |column| attributes = columns.map do |column|
name = column.name name = column.name
value = case name.to_s value = case name.to_s
when @reflection.primary_key_name.to_s when @reflection.foreign_key.to_s
@owner.id @owner.id
when @reflection.association_foreign_key.to_s when @reflection.association_foreign_key.to_s
record.id record.id
@ -64,7 +64,7 @@ module ActiveRecord
records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) } records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) }
else else
relation = Arel::Table.new(@reflection.options[:join_table]) relation = Arel::Table.new(@reflection.options[:join_table])
stmt = relation.where(relation[@reflection.primary_key_name].eq(@owner.id). stmt = relation.where(relation[@reflection.foreign_key].eq(@owner.id).
and(relation[@reflection.association_foreign_key].in(records.map { |x| x.id }.compact)) and(relation[@reflection.association_foreign_key].in(records.map { |x| x.id }.compact))
).compile_delete ).compile_delete
@owner.connection.delete stmt.to_sql @owner.connection.delete stmt.to_sql

View file

@ -14,9 +14,9 @@ module ActiveRecord
def construct_owner_attributes(reflection = @reflection) def construct_owner_attributes(reflection = @reflection)
attributes = {} attributes = {}
if reflection.macro == :belongs_to if reflection.macro == :belongs_to
attributes[reflection.association_primary_key] = @owner.send(reflection.primary_key_name) attributes[reflection.association_primary_key] = @owner.send(reflection.foreign_key)
else else
attributes[reflection.primary_key_name] = @owner.send(reflection.active_record_primary_key) attributes[reflection.foreign_key] = @owner.send(reflection.active_record_primary_key)
if reflection.options[:as] if reflection.options[:as]
attributes["#{reflection.options[:as]}_type"] = @owner.class.base_class.name attributes["#{reflection.options[:as]}_type"] = @owner.class.base_class.name

View file

@ -7,14 +7,6 @@ module ActiveRecord
# is provided by its child HasManyThroughAssociation. # is provided by its child HasManyThroughAssociation.
class HasManyAssociation < AssociationCollection #:nodoc: class HasManyAssociation < AssociationCollection #:nodoc:
protected protected
def owner_quoted_id
if @reflection.options[:primary_key]
@owner.class.quote_value(@owner.send(@reflection.options[:primary_key]))
else
@owner.quoted_id
end
end
# Returns the number of records in this collection. # Returns the number of records in this collection.
# #
# If the association has a counter cache it gets that value. Otherwise # If the association has a counter cache it gets that value. Otherwise
@ -66,7 +58,7 @@ module ActiveRecord
when :delete_all when :delete_all
@reflection.klass.delete(records.map { |r| r.id }) @reflection.klass.delete(records.map { |r| r.id })
else else
updates = { @reflection.primary_key_name => nil } updates = { @reflection.foreign_key => nil }
conditions = { @reflection.association_primary_key => records.map { |r| r.id } } conditions = { @reflection.association_primary_key => records.map { |r| r.id } }
with_scope(@scope) do with_scope(@scope) do

View file

@ -31,7 +31,7 @@ module ActiveRecord
protected protected
def target_reflection_has_associated_record? def target_reflection_has_associated_record?
if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.primary_key_name].blank? if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.foreign_key].blank?
false false
else else
true true

View file

@ -38,11 +38,11 @@ module ActiveRecord
@target.destroy if @target.persisted? @target.destroy if @target.persisted?
@owner.clear_association_cache @owner.clear_association_cache
when :nullify when :nullify
@target[@reflection.primary_key_name] = nil @target[@reflection.foreign_key] = nil
@target.save if @owner.persisted? && @target.persisted? @target.save if @owner.persisted? && @target.persisted?
end end
else else
@target[@reflection.primary_key_name] = nil @target[@reflection.foreign_key] = nil
@target.save if @owner.persisted? && @target.persisted? @target.save if @owner.persisted? && @target.persisted?
end end
end end
@ -65,15 +65,6 @@ module ActiveRecord
end end
end end
protected
def owner_quoted_id
if @reflection.options[:primary_key]
@owner.class.quote_value(@owner.send(@reflection.options[:primary_key]))
else
@owner.quoted_id
end
end
private private
def find_target def find_target
options = @reflection.options.dup.slice(:select, :order, :include, :readonly) options = @reflection.options.dup.slice(:select, :order, :include, :readonly)
@ -105,7 +96,7 @@ module ActiveRecord
if replace_existing if replace_existing
replace(record, true) replace(record, true)
else else
record[@reflection.primary_key_name] = @owner.id if @owner.persisted? record[@reflection.foreign_key] = @owner.id if @owner.persisted?
self.target = record self.target = record
set_inverse_instance(record) set_inverse_instance(record)
end end

View file

@ -13,7 +13,7 @@ module ActiveRecord
def stale_target? def stale_target?
if @target && @reflection.through_reflection.macro == :belongs_to && defined?(@through_foreign_key) if @target && @reflection.through_reflection.macro == :belongs_to && defined?(@through_foreign_key)
previous_key = @through_foreign_key.to_s previous_key = @through_foreign_key.to_s
current_key = @owner.send(@reflection.through_reflection.primary_key_name).to_s current_key = @owner.send(@reflection.through_reflection.foreign_key).to_s
previous_key != current_key previous_key != current_key
else else
@ -69,14 +69,14 @@ module ActiveRecord
if @reflection.source_reflection.macro == :belongs_to if @reflection.source_reflection.macro == :belongs_to
reflection_primary_key = @reflection.source_reflection.options[:primary_key] || reflection_primary_key = @reflection.source_reflection.options[:primary_key] ||
@reflection.klass.primary_key @reflection.klass.primary_key
source_primary_key = @reflection.source_reflection.primary_key_name source_primary_key = @reflection.source_reflection.foreign_key
if @reflection.options[:source_type] if @reflection.options[:source_type]
column = @reflection.source_reflection.options[:foreign_type] column = @reflection.source_reflection.options[:foreign_type]
conditions << conditions <<
right[column].eq(@reflection.options[:source_type]) right[column].eq(@reflection.options[:source_type])
end end
else else
reflection_primary_key = @reflection.source_reflection.primary_key_name reflection_primary_key = @reflection.source_reflection.foreign_key
source_primary_key = @reflection.source_reflection.options[:primary_key] || source_primary_key = @reflection.source_reflection.options[:primary_key] ||
@reflection.through_reflection.klass.primary_key @reflection.through_reflection.klass.primary_key
if @reflection.source_reflection.options[:as] if @reflection.source_reflection.options[:as]
@ -100,7 +100,7 @@ module ActiveRecord
raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro) raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro)
join_attributes = { join_attributes = {
@reflection.source_reflection.primary_key_name => @reflection.source_reflection.foreign_key =>
associate.send(@reflection.source_reflection.association_primary_key) associate.send(@reflection.source_reflection.association_primary_key)
} }
@ -158,10 +158,8 @@ module ActiveRecord
alias_method :sql_conditions, :conditions alias_method :sql_conditions, :conditions
def update_stale_state def update_stale_state
construct_scope if stale_target?
if @reflection.through_reflection.macro == :belongs_to if @reflection.through_reflection.macro == :belongs_to
@through_foreign_key = @owner.send(@reflection.through_reflection.primary_key_name) @through_foreign_key = @owner.send(@reflection.through_reflection.foreign_key)
end end
end end
end end

View file

@ -343,8 +343,8 @@ module ActiveRecord
association.destroy association.destroy
else else
key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave) if autosave != false && (new_record? || association.new_record? || association[reflection.foreign_key] != key || autosave)
association[reflection.primary_key_name] = key association[reflection.foreign_key] = key
saved = association.save(:validate => !autosave) saved = association.save(:validate => !autosave)
raise ActiveRecord::Rollback if !saved && autosave raise ActiveRecord::Rollback if !saved && autosave
saved saved
@ -367,7 +367,7 @@ module ActiveRecord
if association.updated? if association.updated?
association_id = association.send(reflection.options[:primary_key] || :id) association_id = association.send(reflection.options[:primary_key] || :id)
self[reflection.primary_key_name] = association_id self[reflection.foreign_key] = association_id
end end
saved if autosave saved if autosave

View file

@ -874,7 +874,12 @@ module ActiveRecord #:nodoc:
def relation #:nodoc: def relation #:nodoc:
@relation ||= Relation.new(self, arel_table) @relation ||= Relation.new(self, arel_table)
finder_needs_type_condition? ? @relation.where(type_condition) : @relation
if finder_needs_type_condition?
@relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
else
@relation
end
end end
# Finder methods must instantiate through this method to work with the # Finder methods must instantiate through this method to work with the
@ -914,10 +919,9 @@ module ActiveRecord #:nodoc:
def type_condition def type_condition
sti_column = arel_table[inheritance_column.to_sym] sti_column = arel_table[inheritance_column.to_sym]
condition = sti_column.eq(sti_name) sti_names = ([self] + descendants).map { |model| model.sti_name }
descendants.each { |subclass| condition = condition.or(sti_column.eq(subclass.sti_name)) }
condition sti_column.in(sti_names)
end end
# Guesses the table name, but does not decorate it with prefix and suffix information. # Guesses the table name, but does not decorate it with prefix and suffix information.

View file

@ -634,7 +634,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
targets.each do |target| targets.each do |target|
join_fixtures["#{label}_#{target}"] = Fixture.new( join_fixtures["#{label}_#{target}"] = Fixture.new(
{ association.primary_key_name => row[primary_key_name], { association.foreign_key => row[primary_key_name],
association.association_foreign_key => Fixtures.identify(target) }, association.association_foreign_key => Fixtures.identify(target) },
nil, @connection) nil, @connection)
end end

View file

@ -196,8 +196,8 @@ module ActiveRecord
@quoted_table_name ||= klass.quoted_table_name @quoted_table_name ||= klass.quoted_table_name
end end
def primary_key_name def foreign_key
@primary_key_name ||= options[:foreign_key] || derive_primary_key_name @foreign_key ||= options[:foreign_key] || derive_foreign_key
end end
def primary_key_column def primary_key_column
@ -251,7 +251,7 @@ module ActiveRecord
false false
end end
def through_reflection_primary_key_name def through_reflection_foreign_key
end end
def source_reflection def source_reflection
@ -298,17 +298,6 @@ module ActiveRecord
!options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many) !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many)
end end
def dependent_conditions(record, base_class, extra_conditions)
dependent_conditions = []
dependent_conditions << "#{primary_key_name} = #{record.send(name).send(:owner_quoted_id)}"
dependent_conditions << "#{options[:as]}_type = '#{base_class.name}'" if options[:as]
dependent_conditions << klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
dependent_conditions << extra_conditions if extra_conditions
dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ")
dependent_conditions = dependent_conditions.gsub('@', '\@')
dependent_conditions
end
# Returns +true+ if +self+ is a +belongs_to+ reflection. # Returns +true+ if +self+ is a +belongs_to+ reflection.
def belongs_to? def belongs_to?
macro == :belongs_to macro == :belongs_to
@ -321,7 +310,7 @@ module ActiveRecord
class_name class_name
end end
def derive_primary_key_name def derive_foreign_key
if belongs_to? if belongs_to?
"#{name}_id" "#{name}_id"
elsif options[:as] elsif options[:as]
@ -403,11 +392,11 @@ module ActiveRecord
end end
def through_reflection_primary_key def through_reflection_primary_key
through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.primary_key_name through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.foreign_key
end end
def through_reflection_primary_key_name def through_reflection_foreign_key
through_reflection.primary_key_name if through_reflection.belongs_to? through_reflection.foreign_key if through_reflection.belongs_to?
end end
private private

View file

@ -211,7 +211,7 @@ module ActiveRecord
group_attr = @group_values group_attr = @group_values
association = @klass.reflect_on_association(group_attr.first.to_sym) association = @klass.reflect_on_association(group_attr.first.to_sym)
associated = group_attr.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations associated = group_attr.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations
group_fields = Array(associated ? association.primary_key_name : group_attr) group_fields = Array(associated ? association.foreign_key : group_attr)
group_aliases = group_fields.map { |field| column_alias_for(field) } group_aliases = group_fields.map { |field| column_alias_for(field) }
group_columns = group_aliases.zip(group_fields).map { |aliaz,field| group_columns = group_aliases.zip(group_fields).map { |aliaz,field|
[aliaz, column_for(field)] [aliaz, column_for(field)]

View file

@ -46,13 +46,17 @@ module ActiveRecord
merged_relation.where_values = merged_wheres merged_relation.where_values = merged_wheres
(Relation::SINGLE_VALUE_METHODS - [:lock]).each do |method| (Relation::SINGLE_VALUE_METHODS - [:lock, :create_with]).each do |method|
value = r.send(:"#{method}_value") value = r.send(:"#{method}_value")
merged_relation.send(:"#{method}_value=", value) unless value.nil? merged_relation.send(:"#{method}_value=", value) unless value.nil?
end end
merged_relation.lock_value = r.lock_value unless merged_relation.lock_value merged_relation.lock_value = r.lock_value unless merged_relation.lock_value
if r.create_with_value
merged_relation.create_with_value = (merged_relation.create_with_value || {}).merge(r.create_with_value)
end
# Apply scope extension modules # Apply scope extension modules
merged_relation.send :apply_modules, r.extensions merged_relation.send :apply_modules, r.extensions

View file

@ -198,16 +198,23 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Post.find(p.id).comments.size assert_equal 1, Post.find(p.id).comments.size
end end
def test_belongs_to_with_primary_key_counter_with_assigning_nil def test_belongs_to_with_primary_key_counter
debate = Topic.create("title" => "debate") debate = Topic.create("title" => "debate")
reply = Reply.create("title" => "blah!", "content" => "world around!", "parent_title" => "debate") debate2 = Topic.create("title" => "debate2")
reply = Reply.create("title" => "blah!", "content" => "world around!", "parent_title" => "debate")
assert_equal debate.title, reply.parent_title assert_equal 1, debate.reload.replies_count
assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count") assert_equal 0, debate2.reload.replies_count
reply.topic_with_primary_key = debate2
assert_equal 0, debate.reload.replies_count
assert_equal 1, debate2.reload.replies_count
reply.topic_with_primary_key = nil reply.topic_with_primary_key = nil
assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count") assert_equal 0, debate.reload.replies_count
assert_equal 0, debate2.reload.replies_count
end end
def test_belongs_to_counter_with_reassigning def test_belongs_to_counter_with_reassigning
@ -419,6 +426,18 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_nil sponsor.sponsorable_id assert_nil sponsor.sponsorable_id
end end
def test_assignment_updates_foreign_id_field_for_new_and_saved_records
client = Client.new
saved_firm = Firm.create :name => "Saved"
new_firm = Firm.new
client.firm = saved_firm
assert_equal saved_firm.id, client.client_of
client.firm = new_firm
assert_nil client.client_of
end
def test_polymorphic_assignment_with_primary_key_updates_foreign_id_field_for_new_and_saved_records def test_polymorphic_assignment_with_primary_key_updates_foreign_id_field_for_new_and_saved_records
essay = Essay.new essay = Essay.new
saved_writer = Author.create(:name => "David") saved_writer = Author.create(:name => "David")
@ -537,4 +556,27 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert proxy.stale_target? assert proxy.stale_target?
assert_equal companies(:first_firm), sponsor.sponsorable assert_equal companies(:first_firm), sponsor.sponsorable
end end
def test_reloading_association_with_key_change
client = companies(:second_client)
firm = client.firm # note this is a proxy object
client.firm = companies(:another_firm)
assert_equal companies(:another_firm), firm.reload
client.client_of = companies(:first_firm).id
assert_equal companies(:first_firm), firm.reload
end
def test_polymorphic_counter_cache
tagging = taggings(:welcome_general)
post = posts(:welcome)
comment = comments(:greetings)
assert_difference 'post.reload.taggings_count', -1 do
assert_difference 'comment.reload.taggings_count', +1 do
tagging.taggable = comment
end
end
end
end end

View file

@ -3,6 +3,7 @@ require 'models/post'
require 'models/author' require 'models/author'
require 'models/project' require 'models/project'
require 'models/developer' require 'models/developer'
require 'models/company'
class AssociationCallbacksTest < ActiveRecord::TestCase class AssociationCallbacksTest < ActiveRecord::TestCase
fixtures :posts, :authors, :projects, :developers fixtures :posts, :authors, :projects, :developers
@ -81,6 +82,14 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
assert_equal callback_log, jack.post_log assert_equal callback_log, jack.post_log
end end
def test_has_many_callbacks_for_destroy_on_parent
firm = Firm.create! :name => "Firm"
client = firm.clients.create! :name => "Client"
firm.destroy
assert_equal ["before_remove#{client.id}", "after_remove#{client.id}"], firm.log
end
def test_has_and_belongs_to_many_add_callback def test_has_and_belongs_to_many_add_callback
david = developers(:david) david = developers(:david)
ar = projects(:active_record) ar = projects(:active_record)

View file

@ -101,9 +101,10 @@ class LifecycleTest < ActiveRecord::TestCase
fixtures :topics, :developers, :minimalistics fixtures :topics, :developers, :minimalistics
def test_before_destroy def test_before_destroy
original_count = Topic.count topic = Topic.find(1)
(topic_to_be_destroyed = Topic.find(1)).destroy assert_difference 'Topic.count', -(1 + topic.replies.size) do
assert_equal original_count - (1 + topic_to_be_destroyed.replies.size), Topic.count topic.destroy
end
end end
def test_auto_observer def test_auto_observer

View file

@ -8,6 +8,8 @@ require 'models/ship'
require 'models/pirate' require 'models/pirate'
require 'models/price_estimate' require 'models/price_estimate'
require 'models/tagging' require 'models/tagging'
require 'models/author'
require 'models/post'
class ReflectionTest < ActiveRecord::TestCase class ReflectionTest < ActiveRecord::TestCase
include ActiveRecord::Reflection include ActiveRecord::Reflection
@ -127,11 +129,11 @@ class ReflectionTest < ActiveRecord::TestCase
def test_belongs_to_inferred_foreign_key_from_assoc_name def test_belongs_to_inferred_foreign_key_from_assoc_name
Company.belongs_to :foo Company.belongs_to :foo
assert_equal "foo_id", Company.reflect_on_association(:foo).primary_key_name assert_equal "foo_id", Company.reflect_on_association(:foo).foreign_key
Company.belongs_to :bar, :class_name => "Xyzzy" Company.belongs_to :bar, :class_name => "Xyzzy"
assert_equal "bar_id", Company.reflect_on_association(:bar).primary_key_name assert_equal "bar_id", Company.reflect_on_association(:bar).foreign_key
Company.belongs_to :baz, :class_name => "Xyzzy", :foreign_key => "xyzzy_id" Company.belongs_to :baz, :class_name => "Xyzzy", :foreign_key => "xyzzy_id"
assert_equal "xyzzy_id", Company.reflect_on_association(:baz).primary_key_name assert_equal "xyzzy_id", Company.reflect_on_association(:baz).foreign_key
end end
def test_association_reflection_in_modules def test_association_reflection_in_modules

View file

@ -485,4 +485,11 @@ class DefaultScopingTest < ActiveRecord::TestCase
posts_offset_limit = Post.offset(2).limit(3) posts_offset_limit = Post.offset(2).limit(3)
assert_equal posts_limit_offset, posts_offset_limit assert_equal posts_limit_offset, posts_offset_limit
end end
def test_create_with_merge
aaron = (PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20) &
PoorDeveloperCalledJamis.create_with(:name => 'Aaron')).new
assert_equal 20, aaron.salary
assert_equal 'Aaron', aaron.name
end
end end

View file

@ -38,7 +38,9 @@ end
class Firm < Company class Firm < Company
has_many :clients, :order => "id", :dependent => :destroy, :counter_sql => has_many :clients, :order => "id", :dependent => :destroy, :counter_sql =>
"SELECT COUNT(*) FROM companies WHERE firm_id = 1 " + "SELECT COUNT(*) FROM companies WHERE firm_id = 1 " +
"AND (#{QUOTED_TYPE} = 'Client' OR #{QUOTED_TYPE} = 'SpecialClient' OR #{QUOTED_TYPE} = 'VerySpecialClient' )" "AND (#{QUOTED_TYPE} = 'Client' OR #{QUOTED_TYPE} = 'SpecialClient' OR #{QUOTED_TYPE} = 'VerySpecialClient' )",
:before_remove => :log_before_remove,
:after_remove => :log_after_remove
has_many :unsorted_clients, :class_name => "Client" has_many :unsorted_clients, :class_name => "Client"
has_many :clients_sorted_desc, :class_name => "Client", :order => "id DESC" has_many :clients_sorted_desc, :class_name => "Client", :order => "id DESC"
has_many :clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id" has_many :clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id"
@ -88,6 +90,19 @@ class Firm < Company
has_one :unautosaved_account, :foreign_key => "firm_id", :class_name => 'Account', :autosave => false has_one :unautosaved_account, :foreign_key => "firm_id", :class_name => 'Account', :autosave => false
has_many :accounts has_many :accounts
has_many :unautosaved_accounts, :foreign_key => "firm_id", :class_name => 'Account', :autosave => false has_many :unautosaved_accounts, :foreign_key => "firm_id", :class_name => 'Account', :autosave => false
def log
@log ||= []
end
private
def log_before_remove(record)
log << "before_remove#{record.id}"
end
def log_after_remove(record)
log << "after_remove#{record.id}"
end
end end
class DependentFirm < Company class DependentFirm < Company

View file

@ -143,6 +143,7 @@ ActiveRecord::Schema.define do
t.text :body, :null => false t.text :body, :null => false
end end
t.string :type t.string :type
t.integer :taggings_count, :default => 0
end end
create_table :companies, :force => true do |t| create_table :companies, :force => true do |t|