提交 2d9626fc 编写于 作者: J Jon Leighton 提交者: Aaron Patterson

Improved strategy for updating a belongs_to association when the foreign key...

Improved strategy for updating a belongs_to association when the foreign key changes. Rather than resetting each affected association when the foreign key changes, we should lazily check for 'staleness' (where fk does not match target id) when the association is accessed.
上级 3f17ed40
...@@ -1229,14 +1229,12 @@ def belongs_to(association_id, options = {}) ...@@ -1229,14 +1229,12 @@ def belongs_to(association_id, options = {})
if reflection.options[:polymorphic] if reflection.options[:polymorphic]
association_accessor_methods(reflection, BelongsToPolymorphicAssociation) association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
association_foreign_type_setter_method(reflection)
else else
association_accessor_methods(reflection, BelongsToAssociation) association_accessor_methods(reflection, BelongsToAssociation)
association_constructor_method(:build, reflection, BelongsToAssociation) association_constructor_method(:build, reflection, BelongsToAssociation)
association_constructor_method(:create, reflection, BelongsToAssociation) association_constructor_method(:create, reflection, BelongsToAssociation)
end end
association_foreign_key_setter_method(reflection)
add_counter_cache_callbacks(reflection) if options[:counter_cache] add_counter_cache_callbacks(reflection) if options[:counter_cache]
add_touch_callbacks(reflection, options[:touch]) if options[:touch] add_touch_callbacks(reflection, options[:touch]) if options[:touch]
...@@ -1456,7 +1454,7 @@ def association_accessor_methods(reflection, association_proxy_class) ...@@ -1456,7 +1454,7 @@ def association_accessor_methods(reflection, association_proxy_class)
force_reload = params.first unless params.empty? force_reload = params.first unless params.empty?
association = association_instance_get(reflection.name) association = association_instance_get(reflection.name)
if association.nil? || force_reload if association.nil? || force_reload || association.stale_target?
association = association_proxy_class.new(self, reflection) association = association_proxy_class.new(self, reflection)
retval = force_reload ? reflection.klass.uncached { association.reload } : association.reload retval = force_reload ? reflection.klass.uncached { association.reload } : association.reload
if retval.nil? and association_proxy_class == BelongsToAssociation if retval.nil? and association_proxy_class == BelongsToAssociation
...@@ -1556,45 +1554,6 @@ def association_constructor_method(constructor, reflection, association_proxy_cl ...@@ -1556,45 +1554,6 @@ def association_constructor_method(constructor, reflection, association_proxy_cl
end end
end end
def association_foreign_key_setter_method(reflection)
setters = reflect_on_all_associations(:belongs_to).map do |belongs_to_reflection|
if belongs_to_reflection.primary_key_name == reflection.primary_key_name
"association_instance_set(:#{belongs_to_reflection.name}, nil);"
end
end.compact.join
if method_defined?(:"#{reflection.primary_key_name}=")
undef_method :"#{reflection.primary_key_name}="
end
class_eval <<-FILE, __FILE__, __LINE__ + 1
def #{reflection.primary_key_name}=(new_id)
write_attribute :#{reflection.primary_key_name}, new_id
if #{reflection.primary_key_name}_changed?
#{ setters }
end
end
FILE
end
def association_foreign_type_setter_method(reflection)
setters = reflect_on_all_associations(:belongs_to).map do |belongs_to_reflection|
if belongs_to_reflection.options[:foreign_type] == reflection.options[:foreign_type]
"association_instance_set(:#{belongs_to_reflection.name}, nil);"
end
end.compact.join
field = reflection.options[:foreign_type]
class_eval <<-FILE, __FILE__, __LINE__ + 1
def #{field}=(new_id)
write_attribute :#{field}, new_id
if #{field}_changed?
#{ setters }
end
end
FILE
end
def add_counter_cache_callbacks(reflection) def add_counter_cache_callbacks(reflection)
cache_column = reflection.counter_cache_column cache_column = reflection.counter_cache_column
......
...@@ -130,6 +130,16 @@ def loaded ...@@ -130,6 +130,16 @@ def loaded
@loaded = true @loaded = true
end end
# The target is stale if the target no longer points to the record(s) that the
# relevant foreign_key(s) refers to. If stale, the association accessor method
# on the owner will reload the target. It's up to subclasses to implement this
# method if relevant.
#
# Note that if the target has not been loaded, it is not considered stale.
def stale_target?
false
end
# Returns the target of this proxy, same as +proxy_target+. # Returns the target of this proxy, same as +proxy_target+.
def target def target
@target @target
......
...@@ -42,6 +42,14 @@ def updated? ...@@ -42,6 +42,14 @@ def updated?
@updated @updated
end end
def stale_target?
if @target && @target.persisted?
@target.send(@reflection.association_primary_key).to_i != @owner.send(@reflection.primary_key_name).to_i
else
false
end
end
private private
def find_target def find_target
find_method = if @reflection.options[:primary_key] find_method = if @reflection.options[:primary_key]
......
...@@ -23,6 +23,15 @@ def updated? ...@@ -23,6 +23,15 @@ def updated?
@updated @updated
end end
def stale_target?
if @target && @target.persisted?
@target.send(@reflection.association_primary_key).to_i != @owner.send(@reflection.primary_key_name).to_i ||
@target.class.base_class.name.to_s != @owner.send(@reflection.options[:foreign_type]).to_s
else
false
end
end
private private
# 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
......
...@@ -209,7 +209,10 @@ def association_foreign_key ...@@ -209,7 +209,10 @@ def association_foreign_key
end end
def association_primary_key def association_primary_key
@association_primary_key ||= options[:primary_key] || klass.primary_key @association_primary_key ||=
options[:primary_key] ||
!options[:polymorphic] && klass.primary_key ||
'id'
end end
def active_record_primary_key def active_record_primary_key
......
...@@ -298,7 +298,7 @@ def test_should_accept_update_only_option ...@@ -298,7 +298,7 @@ def test_should_accept_update_only_option
def test_should_create_new_model_when_nothing_is_there_and_update_only_is_true def test_should_create_new_model_when_nothing_is_there_and_update_only_is_true
@ship.delete @ship.delete
@pirate.reload.update_attributes(:update_only_ship_attributes => { :name => 'Mayflower' }) @pirate.reload.update_attributes(:update_only_ship_attributes => { :name => 'Mayflower' })
assert_not_nil @pirate.ship assert_not_nil @pirate.ship
...@@ -411,6 +411,7 @@ def test_should_take_a_hash_with_string_keys_and_update_the_associated_model ...@@ -411,6 +411,7 @@ def test_should_take_a_hash_with_string_keys_and_update_the_associated_model
def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id
@pirate.stubs(:id).returns('ABC1X') @pirate.stubs(:id).returns('ABC1X')
@ship.stubs(:pirate_id).returns(@pirate.id) # prevents pirate from being reloaded due to non-matching foreign key
@ship.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' } @ship.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' }
assert_equal 'Arr', @ship.pirate.catchphrase assert_equal 'Arr', @ship.pirate.catchphrase
...@@ -451,7 +452,7 @@ def test_should_work_with_update_attributes_as_well ...@@ -451,7 +452,7 @@ def test_should_work_with_update_attributes_as_well
def test_should_not_destroy_the_associated_model_until_the_parent_is_saved def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
pirate = @ship.pirate pirate = @ship.pirate
@ship.attributes = { :pirate_attributes => { :id => pirate.id, '_destroy' => true } } @ship.attributes = { :pirate_attributes => { :id => pirate.id, '_destroy' => true } }
assert_nothing_raised(ActiveRecord::RecordNotFound) { Pirate.find(pirate.id) } assert_nothing_raised(ActiveRecord::RecordNotFound) { Pirate.find(pirate.id) }
@ship.save @ship.save
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
require 'models/ship' require 'models/ship'
require 'models/pirate' require 'models/pirate'
require 'models/price_estimate' require 'models/price_estimate'
require 'models/tagging'
class ReflectionTest < ActiveRecord::TestCase class ReflectionTest < ActiveRecord::TestCase
include ActiveRecord::Reflection include ActiveRecord::Reflection
...@@ -194,6 +195,7 @@ def test_has_many_through_reflection ...@@ -194,6 +195,7 @@ def test_has_many_through_reflection
def test_association_primary_key def test_association_primary_key
assert_equal "id", Author.reflect_on_association(:posts).association_primary_key.to_s assert_equal "id", Author.reflect_on_association(:posts).association_primary_key.to_s
assert_equal "name", Author.reflect_on_association(:essay).association_primary_key.to_s assert_equal "name", Author.reflect_on_association(:essay).association_primary_key.to_s
assert_equal "id", Tagging.reflect_on_association(:taggable).association_primary_key.to_s
end end
def test_active_record_primary_key def test_active_record_primary_key
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册