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

If a counter_cache is defined, then using update_attributes and changing

the primary key on an association will make sure that the corresponding
counter on the association is changed properly. Fixes #9722.
This commit is contained in:
John Wang 2013-03-15 11:48:53 -04:00 committed by wangjohn
parent ae8e84e976
commit 455d710242
3 changed files with 77 additions and 0 deletions

View file

@ -1,5 +1,31 @@
## Rails 4.0.0 (unreleased) ## ## Rails 4.0.0 (unreleased) ##
* Counter caches on associations will now stay valid when attributes are
updated (not just when records are created or destroyed), for example,
when calling +update_attributes+. The following code now works:
class Comment < ActiveRecord::Base
belongs_to :post, counter_cache: true
end
class Post < ActiveRecord::Base
has_many :comments
end
post = Post.create
comment = Comment.create
post.comments << comment
post.save.reload.comments_count # => 1
comment.update_attributes(:post_id => nil)
post.save.reload.comments_count # => 0
Updating the id of a +belongs_to+ object with the id of a new object will
also keep the count accurate.
*John Wang*
* Referencing join tables implicitly was deprecated. There is a * Referencing join tables implicitly was deprecated. There is a
possibility that these deprecation warnings are shown even if you possibility that these deprecation warnings are shown even if you
don't make use of that feature. You can now disable the feature entirely. don't make use of that feature. You can now disable the feature entirely.

View file

@ -21,11 +21,13 @@ module ActiveRecord::Associations::Builder
def add_counter_cache_callbacks(reflection) def add_counter_cache_callbacks(reflection)
cache_column = reflection.counter_cache_column cache_column = reflection.counter_cache_column
foreign_key = reflection.foreign_key
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def belongs_to_counter_cache_after_create_for_#{name} def belongs_to_counter_cache_after_create_for_#{name}
record = #{name} record = #{name}
record.class.increment_counter(:#{cache_column}, record.id) unless record.nil? record.class.increment_counter(:#{cache_column}, record.id) unless record.nil?
@_after_create_counter_called = true
end end
def belongs_to_counter_cache_before_destroy_for_#{name} def belongs_to_counter_cache_before_destroy_for_#{name}
@ -34,10 +36,28 @@ module ActiveRecord::Associations::Builder
record.class.decrement_counter(:#{cache_column}, record.id) unless record.nil? record.class.decrement_counter(:#{cache_column}, record.id) unless record.nil?
end end
end end
def belongs_to_counter_cache_after_update_for_#{name}
if (@_after_create_counter_called ||= false)
@_after_create_counter_called = false
elsif self.#{foreign_key}_changed? && !new_record? && defined?(#{name.to_s.camelize})
model = #{name.to_s.camelize}
foreign_key_was = self.#{foreign_key}_was
foreign_key = self.#{foreign_key}
if foreign_key && model.respond_to?(:increment_counter)
model.increment_counter(:#{cache_column}, foreign_key)
end
if foreign_key_was && model.respond_to?(:decrement_counter)
model.decrement_counter(:#{cache_column}, foreign_key_was)
end
end
end
CODE CODE
model.after_create "belongs_to_counter_cache_after_create_for_#{name}" model.after_create "belongs_to_counter_cache_after_create_for_#{name}"
model.before_destroy "belongs_to_counter_cache_before_destroy_for_#{name}" model.before_destroy "belongs_to_counter_cache_before_destroy_for_#{name}"
model.after_update "belongs_to_counter_cache_after_update_for_#{name}"
klass = reflection.class_name.safe_constantize klass = reflection.class_name.safe_constantize
klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly) klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly)

View file

@ -789,6 +789,37 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end end
end end
def test_calling_update_attributes_on_id_changes_the_counter_cache
topic = Topic.order("id ASC").first
original_count = topic.replies.to_a.size
assert_equal original_count, topic.replies_count
first_reply = topic.replies.first
first_reply.update_attributes(:parent_id => nil)
assert_equal original_count - 1, topic.reload.replies_count
first_reply.update_attributes(:parent_id => topic.id)
assert_equal original_count, topic.reload.replies_count
end
def test_calling_update_attributes_changing_ids_doesnt_change_counter_cache
topic1 = Topic.find(1)
topic2 = Topic.find(3)
original_count1 = topic1.replies.to_a.size
original_count2 = topic2.replies.to_a.size
reply1 = topic1.replies.first
reply2 = topic2.replies.first
reply1.update_attributes(:parent_id => topic2.id)
assert_equal original_count1 - 1, topic1.reload.replies_count
assert_equal original_count2 + 1, topic2.reload.replies_count
reply2.update_attributes(:parent_id => topic1.id)
assert_equal original_count1, topic1.reload.replies_count
assert_equal original_count2, topic2.reload.replies_count
end
def test_deleting_a_collection def test_deleting_a_collection
force_signal37_to_load_all_clients_of_firm force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.create("name" => "Another Client") companies(:first_firm).clients_of_firm.create("name" => "Another Client")