counter_culture 3.6.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|