counter_culture 3.6.0 → 3.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +102 -0
- data/lib/counter_culture/counter.rb +81 -9
- data/lib/counter_culture/version.rb +1 -1
- data/lib/counter_culture.rb +32 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 88687e8c584a1643026e8e09d9f64484110fd7a19ebf1faa6cb311361e8c35ca
|
4
|
+
data.tar.gz: a81ac645489129472e265f3bfefa0573f11a9e8419a40ba7f4c687c692367177
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 82ffa1af6e6ad80f68a56b77cdae1f68dd30aa63535281f4ee19ee3ad1e5f88cc7053ae4cab6f1786db7038baeb81cf5c434b1b657f886b62862d79b0d99d7dc
|
7
|
+
data.tar.gz: 83e5c626c97583727a5cbef6c824dd5eb25625d169a53e65e73bd1d2ab633121bb2a1a2de430a2d522caa92382b5852d731e0f954818fa906ab2b0060986119a
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -285,6 +285,108 @@ You can also pass a `Proc` for dynamic control. This is useful for temporarily m
|
|
285
285
|
counter_culture :category, execute_after_commit: proc { !Thread.current[:update_counter_cache_in_transaction] }
|
286
286
|
```
|
287
287
|
|
288
|
+
### Aggregating multiple updates into single SQL queries
|
289
|
+
|
290
|
+
> **NOTE**: This does not have an effect on Papertrail callbacks
|
291
|
+
|
292
|
+
By default, every create/update/destroy will trigger separate `UPDATE` SQLs for each action and targeted column. For example, if the creation of 1 record needs to increment 3 counter cache columns, a transaction that creates 3 records of this kind will generate 9 `UPDATE` SQL queries adding `+1` to the value of said columns (3 created records x 3 counter cache columns to be incremented by each).
|
293
|
+
|
294
|
+
To avoid this, you can wrap the logic that creates/updates/destroys multiple records in `CounterCulture.aggregate_counter_updates`. This will sum all updates for counter cache columns and will take the latest value for timestamp updates. As a result, you will get just one `UPDATE` SQL query per target record that updates all of its columns at once.
|
295
|
+
|
296
|
+
#### Execute aggregated SQL queries before transaction `COMMIT`
|
297
|
+
```ruby
|
298
|
+
ActiveRecord::Base.transaction do
|
299
|
+
CounterCulture.aggregate_counter_updates do
|
300
|
+
# list of updates
|
301
|
+
end # => executes aggregated SQLs
|
302
|
+
end
|
303
|
+
```
|
304
|
+
|
305
|
+
#### Execute aggregated SQL queries after transaction `COMMIT`
|
306
|
+
```ruby
|
307
|
+
CounterCulture.aggregate_counter_updates do
|
308
|
+
ActiveRecord::Base.transaction do
|
309
|
+
# list of updates
|
310
|
+
end
|
311
|
+
end # => executes aggregated SQLs
|
312
|
+
```
|
313
|
+
|
314
|
+
#### Examples:
|
315
|
+
<details><summary>Sums multiple counter updates for single column of a single target record</summary>
|
316
|
+
|
317
|
+
```sql
|
318
|
+
-- Before
|
319
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1 WHERE `authors`.`id` = 1
|
320
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1 WHERE `authors`.`id` = 1
|
321
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1 WHERE `authors`.`id` = 1
|
322
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) - 1 WHERE `authors`.`id` = 1
|
323
|
+
|
324
|
+
-- After
|
325
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 2 WHERE `authors`.`id` = 1
|
326
|
+
```
|
327
|
+
|
328
|
+
```sql
|
329
|
+
-- Before
|
330
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) - 1 WHERE `authors`.`id` = 1
|
331
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) - 1 WHERE `authors`.`id` = 1
|
332
|
+
|
333
|
+
-- After
|
334
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + -2 WHERE `authors`.`id` = 1
|
335
|
+
```
|
336
|
+
|
337
|
+
```sql
|
338
|
+
-- Before
|
339
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1 WHERE `authors`.`id` = 1
|
340
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) - 1 WHERE `authors`.`id` = 1
|
341
|
+
|
342
|
+
-- After
|
343
|
+
No query
|
344
|
+
```
|
345
|
+
|
346
|
+
</details>
|
347
|
+
|
348
|
+
<details><summary>Combines multiple updates for multiple columns of a single target record</summary>
|
349
|
+
|
350
|
+
```sql
|
351
|
+
-- Before
|
352
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1, updated_at = '2024-06-06 01:00:00' WHERE `authors`.`id` = 1
|
353
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1, updated_at = '2024-06-06 02:00:00' WHERE `authors`.`id` = 1
|
354
|
+
|
355
|
+
-- After
|
356
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 2, updated_at = '2024-06-06 02:00:00' WHERE `authors`.`id` = 1
|
357
|
+
```
|
358
|
+
|
359
|
+
```sql
|
360
|
+
-- Before
|
361
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1 WHERE `authors`.`id` = 1
|
362
|
+
UPDATE `authors` SET `authors`.`works_count` = COALESCE(`authors`.`works_count`, 0) + 1 WHERE `authors`.`id` = 1
|
363
|
+
|
364
|
+
-- After
|
365
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1, `authors`.`works_count` = COALESCE(`authors`.`works_count`, 0) + 1 WHERE `authors`.`id` = 1
|
366
|
+
```
|
367
|
+
|
368
|
+
</details>
|
369
|
+
|
370
|
+
</details>
|
371
|
+
|
372
|
+
<details><summary>Combines multiple updates of multiple target records</summary>
|
373
|
+
|
374
|
+
```sql
|
375
|
+
-- Before
|
376
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) - 1 WHERE `authors`.`id` = 1
|
377
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) - 1 WHERE `authors`.`id` = 1
|
378
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) - 1 WHERE `authors`.`id` = 1
|
379
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1 WHERE `authors`.`id` = 2
|
380
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1 WHERE `authors`.`id` = 2
|
381
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1 WHERE `authors`.`id` = 2
|
382
|
+
|
383
|
+
-- After
|
384
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + -3 WHERE `authors`.`id` = 1
|
385
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 3 WHERE `authors`.`id` = 2
|
386
|
+
```
|
387
|
+
|
388
|
+
</details>
|
389
|
+
|
288
390
|
### Manually populating counter cache values
|
289
391
|
|
290
392
|
You will sometimes want to populate counter-cache values from primary data. This is required when adding counter-caches to existing data. It is also recommended to run this regularly (at BestVendor, we run it once a week) to catch any incorrect values in the counter caches.
|
@@ -75,13 +75,15 @@ module CounterCulture
|
|
75
75
|
|
76
76
|
# we don't use Rails' update_counters because we support changing the timestamp
|
77
77
|
updates = []
|
78
|
-
|
79
|
-
|
80
|
-
|
78
|
+
|
79
|
+
# this will update the actual counter
|
80
|
+
updates << if column_type == :money
|
81
|
+
assemble_money_counter_update(klass, id_to_change, quoted_column, operator, delta_magnitude)
|
81
82
|
else
|
82
|
-
|
83
|
+
assemble_counter_update(klass, id_to_change, quoted_column, operator, delta_magnitude)
|
83
84
|
end
|
84
|
-
|
85
|
+
|
86
|
+
# and this will update the timestamp, if so desired
|
85
87
|
if touch
|
86
88
|
current_time = klass.send(:current_time_from_proper_timezone)
|
87
89
|
timestamp_columns = klass.send(:timestamp_attributes_for_update_in_model)
|
@@ -91,12 +93,21 @@ module CounterCulture
|
|
91
93
|
timestamp_columns << touch
|
92
94
|
end
|
93
95
|
timestamp_columns.each do |timestamp_column|
|
94
|
-
updates <<
|
96
|
+
updates << assemble_timestamp_update(
|
97
|
+
klass,
|
98
|
+
id_to_change,
|
99
|
+
timestamp_column,
|
100
|
+
-> { klass.send(:current_time_from_proper_timezone).to_formatted_s(:db) }
|
101
|
+
)
|
95
102
|
end
|
96
103
|
end
|
97
104
|
|
98
105
|
primary_key = relation_primary_key(relation, source: obj, was: options[:was])
|
99
106
|
|
107
|
+
if Thread.current[:aggregate_counter_updates]
|
108
|
+
Thread.current[:primary_key_map][klass] ||= primary_key
|
109
|
+
end
|
110
|
+
|
100
111
|
if @with_papertrail
|
101
112
|
instance = klass.where(primary_key => id_to_change).first
|
102
113
|
if instance
|
@@ -120,8 +131,10 @@ module CounterCulture
|
|
120
131
|
end
|
121
132
|
end
|
122
133
|
|
123
|
-
|
124
|
-
|
134
|
+
unless Thread.current[:aggregate_counter_updates]
|
135
|
+
execute_now_or_after_commit(obj) do
|
136
|
+
klass.where(primary_key => id_to_change).update_all updates.join(', ')
|
137
|
+
end
|
125
138
|
end
|
126
139
|
end
|
127
140
|
end
|
@@ -167,7 +180,7 @@ module CounterCulture
|
|
167
180
|
def foreign_key_value(obj, relation, was = false)
|
168
181
|
original_relation = relation
|
169
182
|
relation = relation.is_a?(Enumerable) ? relation.dup : [relation]
|
170
|
-
|
183
|
+
|
171
184
|
if was
|
172
185
|
first = relation.shift
|
173
186
|
foreign_key_value = attribute_was(obj, relation_foreign_key(first))
|
@@ -335,6 +348,7 @@ module CounterCulture
|
|
335
348
|
end
|
336
349
|
|
337
350
|
private
|
351
|
+
|
338
352
|
def attribute_was(obj, attr)
|
339
353
|
changes_method =
|
340
354
|
if ACTIVE_RECORD_VERSION >= Gem::Version.new("5.1.0")
|
@@ -344,5 +358,63 @@ module CounterCulture
|
|
344
358
|
end
|
345
359
|
obj.public_send("#{attr}#{changes_method}")
|
346
360
|
end
|
361
|
+
|
362
|
+
def assemble_money_counter_update(klass, id_to_change, quoted_column, operator, delta_magnitude)
|
363
|
+
counter_update_snippet(
|
364
|
+
"#{quoted_column} = COALESCE(CAST(#{quoted_column} as NUMERIC), 0)",
|
365
|
+
klass,
|
366
|
+
id_to_change,
|
367
|
+
operator,
|
368
|
+
delta_magnitude
|
369
|
+
)
|
370
|
+
end
|
371
|
+
|
372
|
+
def assemble_counter_update(klass, id_to_change, quoted_column, operator, delta_magnitude)
|
373
|
+
counter_update_snippet(
|
374
|
+
"#{quoted_column} = COALESCE(#{quoted_column}, 0)",
|
375
|
+
klass,
|
376
|
+
id_to_change,
|
377
|
+
operator,
|
378
|
+
delta_magnitude
|
379
|
+
)
|
380
|
+
end
|
381
|
+
|
382
|
+
def assemble_timestamp_update(klass, id_to_change, timestamp_column, value)
|
383
|
+
update = "#{timestamp_column} ="
|
384
|
+
|
385
|
+
if Thread.current[:aggregate_counter_updates]
|
386
|
+
remember_timestamp_update(klass, id_to_change, update, value)
|
387
|
+
else
|
388
|
+
"#{update} '#{value.call}'"
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
def counter_update_snippet(update, klass, id_to_change, operator, delta_magnitude)
|
393
|
+
if Thread.current[:aggregate_counter_updates]
|
394
|
+
remember_counter_update(
|
395
|
+
klass,
|
396
|
+
id_to_change,
|
397
|
+
"#{update} +",
|
398
|
+
operator == '+' ? delta_magnitude : -delta_magnitude
|
399
|
+
)
|
400
|
+
else
|
401
|
+
"#{update} #{operator} #{delta_magnitude}"
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
def remember_counter_update(klass, id, operation, value)
|
406
|
+
Thread.current[:aggregated_updates][klass] ||= {}
|
407
|
+
Thread.current[:aggregated_updates][klass][id] ||= {}
|
408
|
+
Thread.current[:aggregated_updates][klass][id][operation] ||= 0
|
409
|
+
|
410
|
+
Thread.current[:aggregated_updates][klass][id][operation] += value
|
411
|
+
end
|
412
|
+
|
413
|
+
def remember_timestamp_update(klass, id, operation, value)
|
414
|
+
Thread.current[:aggregated_updates][klass] ||= {}
|
415
|
+
Thread.current[:aggregated_updates][klass][id] ||= {}
|
416
|
+
|
417
|
+
Thread.current[:aggregated_updates][klass][id][operation] = value
|
418
|
+
end
|
347
419
|
end
|
348
420
|
end
|
data/lib/counter_culture.rb
CHANGED
@@ -15,6 +15,38 @@ module CounterCulture
|
|
15
15
|
yield(self) if block_given?
|
16
16
|
self
|
17
17
|
end
|
18
|
+
|
19
|
+
def self.aggregate_counter_updates
|
20
|
+
return unless block_given?
|
21
|
+
|
22
|
+
Thread.current[:aggregate_counter_updates] = true
|
23
|
+
Thread.current[:aggregated_updates] = {}
|
24
|
+
Thread.current[:primary_key_map] = {}
|
25
|
+
|
26
|
+
result = yield
|
27
|
+
|
28
|
+
# aggregate the updates for each target record and execute SQL queries
|
29
|
+
Thread.current[:aggregated_updates].each do |klass, attrs|
|
30
|
+
attrs.each do |rec_id, updates|
|
31
|
+
update_snippets = updates.map do |operation, value|
|
32
|
+
value = value.call if value.is_a?(Proc)
|
33
|
+
%Q{#{operation} #{value.is_a?(String) ? "'#{value}'" : value}} unless value == 0
|
34
|
+
end.compact
|
35
|
+
|
36
|
+
if update_snippets.any?
|
37
|
+
klass
|
38
|
+
.where(Thread.current[:primary_key_map][klass] => rec_id)
|
39
|
+
.update_all(update_snippets.join(', '))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
result
|
45
|
+
ensure
|
46
|
+
Thread.current[:aggregate_counter_updates] = false
|
47
|
+
Thread.current[:aggregated_updates] = nil
|
48
|
+
Thread.current[:primary_key_map] = nil
|
49
|
+
end
|
18
50
|
end
|
19
51
|
|
20
52
|
# extend ActiveRecord with our own code here
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: counter_culture
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Magnus von Koeller
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-06-
|
11
|
+
date: 2024-06-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|