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) ##
* 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
possibility that these deprecation warnings are shown even if you
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)
cache_column = reflection.counter_cache_column
foreign_key = reflection.foreign_key
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def belongs_to_counter_cache_after_create_for_#{name}
record = #{name}
record.class.increment_counter(:#{cache_column}, record.id) unless record.nil?
@_after_create_counter_called = true
end
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?
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
model.after_create "belongs_to_counter_cache_after_create_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.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly)

View file

@ -789,6 +789,37 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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
force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.create("name" => "Another Client")