Skip to content

Commit fe0270c

Browse files
authored
Merge pull request rails#44971 from fatkodima/reset_counters-multiple-ids
Allow to reset cache counters for multiple records
2 parents e1c7bf5 + 0e6706c commit fe0270c

File tree

3 files changed

+54
-10
lines changed

3 files changed

+54
-10
lines changed

activerecord/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
* Allow to reset cache counters for multiple records.
2+
3+
```
4+
Aircraft.reset_counters([1, 2, 3], :wheels_count)
5+
```
6+
7+
It produces much fewer queries compared to the custom implementation using looping over ids.
8+
Previously: `O(ids.size * counters.size)` queries, now: `O(ids.size + counters.size)` queries.
9+
10+
*fatkodima*
11+
112
* Add `affected_rows` to `sql.active_record` Notification.
213
314
*Hartley McGuire*

activerecord/lib/active_record/counter_cache.rb

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ module ClassMethods
1717
#
1818
# ==== Parameters
1919
#
20-
# * +id+ - The id of the object you wish to reset a counter on.
20+
# * +id+ - The id of the object you wish to reset a counter on or an array of ids.
2121
# * +counters+ - One or more association counters to reset. Association name or counter name can be given.
2222
# * <tt>:touch</tt> - Touch timestamp columns when updating.
2323
# Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to
@@ -28,13 +28,25 @@ module ClassMethods
2828
# # For the Post with id #1, reset the comments_count
2929
# Post.reset_counters(1, :comments)
3030
#
31+
# # For posts with ids #1 and #2, reset the comments_count
32+
# Post.reset_counters([1, 2], :comments)
33+
#
3134
# # Like above, but also touch the +updated_at+ and/or +updated_on+
3235
# # attributes.
3336
# Post.reset_counters(1, :comments, touch: true)
3437
def reset_counters(id, *counters, touch: nil)
35-
object = find(id)
38+
ids = if composite_primary_key?
39+
if id.first.is_a?(Array)
40+
id
41+
else
42+
[id]
43+
end
44+
else
45+
Array(id)
46+
end
47+
48+
updates = Hash.new { |h, k| h[k] = {} }
3649

37-
updates = {}
3850
counters.each do |counter_association|
3951
has_many_association = _reflect_on_association(counter_association)
4052
unless has_many_association
@@ -48,25 +60,38 @@ def reset_counters(id, *counters, touch: nil)
4860
has_many_association = has_many_association.through_reflection
4961
end
5062

63+
counter_association = counter_association.to_sym
5164
foreign_key = has_many_association.foreign_key.to_s
5265
child_class = has_many_association.klass
5366
reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
5467
counter_name = reflection.counter_cache_column
5568

56-
count_was = object.send(counter_name)
57-
count = object.send(counter_association).count(:all)
58-
updates[counter_name] = count if count != count_was
69+
counts =
70+
unscoped
71+
.joins(counter_association)
72+
.where(primary_key => ids)
73+
.group(primary_key)
74+
.count(:all)
75+
76+
ids.each do |id|
77+
updates[id].merge!(counter_name => counts[id] || 0)
78+
end
5979
end
6080

6181
if touch
6282
names = touch if touch != true
6383
names = Array.wrap(names)
6484
options = names.extract_options!
6585
touch_updates = touch_attributes_with_time(*names, **options)
66-
updates.merge!(touch_updates)
86+
87+
updates.each_value do |record_updates|
88+
record_updates.merge!(touch_updates)
89+
end
6790
end
6891

69-
unscoped.where(primary_key => [object.id]).update_all(updates) if updates.any?
92+
updates.each do |id, record_updates|
93+
unscoped.where(primary_key => [id]).update_all(record_updates)
94+
end
7095

7196
true
7297
end

activerecord/test/cases/counter_cache_test.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@ class ::SpecialReply < ::Reply
103103
end
104104
end
105105

106+
test "reset counters for multiple records" do
107+
t1, t2 = topics(:first, :second)
108+
Topic.increment_counter(:replies_count, [t1.id, t2.id])
109+
110+
assert_difference ["t1.reload.replies_count", "t2.reload.replies_count"], -1 do
111+
Topic.reset_counters([t1.id, t2.id], :replies_count)
112+
end
113+
end
114+
106115
test "reset multiple counters" do
107116
Topic.update_counters @topic.id, replies_count: 1, unique_replies_count: 1
108117
assert_difference ["@topic.reload.replies_count", "@topic.reload.unique_replies_count"], -1 do
@@ -164,10 +173,9 @@ class ::SpecialReply < ::Reply
164173
test "reset counter performs query for correct counter with touch: true" do
165174
Topic.reset_counters(@topic.id, :replies_count)
166175

167-
# SELECT "topics".* FROM "topics" WHERE "topics"."id" = ? LIMIT ?
168176
# SELECT COUNT(*) FROM "topics" WHERE "topics"."type" IN (?, ?, ?, ?, ?) AND "topics"."parent_id" = ?
169177
# UPDATE "topics" SET "updated_at" = ? WHERE "topics"."id" = ?
170-
assert_queries_count(3) do
178+
assert_queries_count(2) do
171179
Topic.reset_counters(@topic.id, :replies_count, touch: true)
172180
end
173181
end

0 commit comments

Comments
 (0)