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:
parent
ae8e84e976
commit
455d710242
3 changed files with 77 additions and 0 deletions
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in a new issue