counter_culture 1.12.0 → 2.0.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 +12 -0
- data/README.md +0 -10
- data/VERSION +1 -1
- data/counter_culture.gemspec +3 -3
- data/lib/counter_culture/counter.rb +35 -43
- data/lib/counter_culture/extensions.rb +20 -43
- data/spec/counter_culture_spec.rb +28 -0
- data/spec/models/review.rb +1 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 626c6d44c921e9bf854359c6f2360683392c8053
|
4
|
+
data.tar.gz: 171c902ee182fbe717f3fee383b2932bc8e31207
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 34dd015103dc4a1da9c49feeda4009da59662ccfd6dc7d972f7c6c05eb0e4dbd72db452c8dc113dbb50903e854dacd6db112a370890f6572013931742e82b989
|
7
|
+
data.tar.gz: 84fd5aefae33c42919e10dd91a8eb4b747d564b38994702f7dfdae76c1810fd5a1d910a5be9cbcf260095838902fed8005bcb360f4447c5c143c1a0d7bdbf778
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
## 2.0.0 (June 12, 2018)
|
2
|
+
|
3
|
+
Breaking changes:
|
4
|
+
- execute_after_commit was removed
|
5
|
+
- Removed workaround for incorrect counts when triggering updates from an `after_create` hook. Your options if this applies to you:
|
6
|
+
* continue using counter_culture 1.12.0
|
7
|
+
* upgrade to Rails 5.1.5 which fixes the underlying issue in Rails
|
8
|
+
* avoid triggering further updates on the same model in `after_create`; simply set the attribute in `before_create` instead
|
9
|
+
|
10
|
+
Bugfixes:
|
11
|
+
- Multiple updates in one transaction will now be processed correctly (#222)
|
12
|
+
|
1
13
|
## 1.12.0 (June 8, 2018)
|
2
14
|
|
3
15
|
Improvements:
|
data/README.md
CHANGED
@@ -216,16 +216,6 @@ You may also specify a custom timestamp column that gets updated only when a par
|
|
216
216
|
|
217
217
|
With this option, any time the `category_counter_cache` changes both the `category_count_changed` and `updated_at` columns will get updated.
|
218
218
|
|
219
|
-
### Executing counter cache updates after commit
|
220
|
-
|
221
|
-
By default, counter_culture will run counter cache updates inside of the same ActiveRecord transaction that triggered it. (Note that this bevavior [changed from version 0.2.3 to 1.0.0](CHANGELOG.md#100-november-15-2016).) If you would like to run counter cache updates outside of that transaction, for example because you are experiencing [deadlocks with older versions of PostgreSQL](http://mina.naguib.ca/blog/2010/11/22/postgresql-foreign-key-deadlocks.html), you can enable that behavior:
|
222
|
-
```ruby
|
223
|
-
counter_culture :category, execute_after_commit: true
|
224
|
-
```
|
225
|
-
|
226
|
-
Please note that using `execute_after_commit` in conjunction with transactional
|
227
|
-
fixtures will lead to your tests no longer seeing updated counter values.
|
228
|
-
|
229
219
|
### Manually populating counter cache values
|
230
220
|
|
231
221
|
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.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
2.0.0
|
data/counter_culture.gemspec
CHANGED
@@ -2,16 +2,16 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: counter_culture
|
5
|
+
# stub: counter_culture 2.0.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "counter_culture"
|
9
|
-
s.version = "
|
9
|
+
s.version = "2.0.0"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib"]
|
13
13
|
s.authors = ["Magnus von Koeller"]
|
14
|
-
s.date = "2018-06-
|
14
|
+
s.date = "2018-06-18"
|
15
15
|
s.description = "counter_culture provides turbo-charged counter caches that are kept up-to-date not just on create and destroy, that support multiple levels of indirection through relationships, allow dynamic column names and that avoid deadlocks by updating in the after_commit callback."
|
16
16
|
s.email = "magnus@vonkoeller.de"
|
17
17
|
s.extra_rdoc_files = [
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module CounterCulture
|
2
2
|
class Counter
|
3
|
-
CONFIG_OPTIONS = [ :column_names, :counter_cache_name, :delta_column, :foreign_key_values, :touch, :delta_magnitude
|
3
|
+
CONFIG_OPTIONS = [ :column_names, :counter_cache_name, :delta_column, :foreign_key_values, :touch, :delta_magnitude]
|
4
4
|
ACTIVE_RECORD_VERSION = Gem.loaded_specs["activerecord"].version
|
5
5
|
|
6
6
|
attr_reader :model, :relation, *CONFIG_OPTIONS
|
@@ -9,13 +9,16 @@ module CounterCulture
|
|
9
9
|
@model = model
|
10
10
|
@relation = relation.is_a?(Enumerable) ? relation : [relation]
|
11
11
|
|
12
|
+
if options.fetch(:execute_after_commit, false)
|
13
|
+
fail("execute_after_commit was removed; updates now run within the transaction")
|
14
|
+
end
|
15
|
+
|
12
16
|
@counter_cache_name = options.fetch(:column_name, "#{model.name.tableize}_count")
|
13
17
|
@column_names = options[:column_names]
|
14
18
|
@delta_column = options[:delta_column]
|
15
19
|
@foreign_key_values = options[:foreign_key_values]
|
16
20
|
@touch = options.fetch(:touch, false)
|
17
21
|
@delta_magnitude = options[:delta_magnitude] || 1
|
18
|
-
@execute_after_commit = options.fetch(:execute_after_commit, false)
|
19
22
|
@with_papertrail = options.fetch(:with_papertrail, false)
|
20
23
|
end
|
21
24
|
|
@@ -29,7 +32,6 @@ module CounterCulture
|
|
29
32
|
# :delta_column => override the default count delta (1) with the value of this column in the counted record
|
30
33
|
# :was => whether to get the current value or the old value of the
|
31
34
|
# first part of the relation
|
32
|
-
# :execute_after_commit => execute the column update outside of the transaction to avoid deadlocks
|
33
35
|
# :with_papertrail => update the column via Papertrail touch_with_version method
|
34
36
|
def change_counter_cache(obj, options)
|
35
37
|
change_counter_column = options.fetch(:counter_column) { counter_cache_name_for(obj) }
|
@@ -45,43 +47,41 @@ module CounterCulture
|
|
45
47
|
else
|
46
48
|
counter_delta_magnitude_for(obj)
|
47
49
|
end
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
updates << "#{timestamp_column} = '#{current_time.to_formatted_s(:db)}'"
|
65
|
-
end
|
50
|
+
# increment or decrement?
|
51
|
+
operator = options[:increment] ? '+' : '-'
|
52
|
+
|
53
|
+
# we don't use Rails' update_counters because we support changing the timestamp
|
54
|
+
quoted_column = model.connection.quote_column_name(change_counter_column)
|
55
|
+
|
56
|
+
updates = []
|
57
|
+
# this updates the actual counter
|
58
|
+
updates << "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{delta_magnitude}"
|
59
|
+
# and here we update the timestamp, if so desired
|
60
|
+
if touch
|
61
|
+
current_time = obj.send(:current_time_from_proper_timezone)
|
62
|
+
timestamp_columns = obj.send(:timestamp_attributes_for_update_in_model)
|
63
|
+
timestamp_columns << touch if touch != true
|
64
|
+
timestamp_columns.each do |timestamp_column|
|
65
|
+
updates << "#{timestamp_column} = '#{current_time.to_formatted_s(:db)}'"
|
66
66
|
end
|
67
|
+
end
|
67
68
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
end
|
69
|
+
klass = relation_klass(relation, source: obj, was: options[:was])
|
70
|
+
primary_key = relation_primary_key(relation, source: obj, was: options[:was])
|
71
|
+
|
72
|
+
if @with_papertrail
|
73
|
+
instance = klass.where(primary_key => id_to_change).first
|
74
|
+
if instance
|
75
|
+
if instance.paper_trail.respond_to?(:save_with_version)
|
76
|
+
# touch_with_version is deprecated starting in PaperTrail 9.0.0
|
77
|
+
instance.paper_trail.save_with_version(validate: false)
|
78
|
+
else
|
79
|
+
instance.paper_trail.touch_with_version
|
80
80
|
end
|
81
81
|
end
|
82
|
-
|
83
|
-
klass.where(primary_key => id_to_change).update_all updates.join(', ')
|
84
82
|
end
|
83
|
+
|
84
|
+
klass.where(primary_key => id_to_change).update_all updates.join(', ')
|
85
85
|
end
|
86
86
|
end
|
87
87
|
|
@@ -278,14 +278,6 @@ module CounterCulture
|
|
278
278
|
end
|
279
279
|
|
280
280
|
private
|
281
|
-
def execute_change_counter_cache(obj, options)
|
282
|
-
if execute_after_commit
|
283
|
-
obj.execute_after_commit { yield }
|
284
|
-
else
|
285
|
-
yield
|
286
|
-
end
|
287
|
-
end
|
288
|
-
|
289
281
|
def attribute_was(obj, attr)
|
290
282
|
changes_method =
|
291
283
|
if ACTIVE_RECORD_VERSION >= Gem::Version.new("5.1.0")
|
@@ -89,61 +89,38 @@ module CounterCulture
|
|
89
89
|
end
|
90
90
|
|
91
91
|
private
|
92
|
-
# need to make sure counter_culture is only activated once
|
93
|
-
# per commit; otherwise, if we do an update in an after_create,
|
94
|
-
# we would be triggered twice within the same transaction -- once
|
95
|
-
# for the create, once for the update
|
96
|
-
def _wrap_in_counter_culture_active(&block)
|
97
|
-
if @_counter_culture_active
|
98
|
-
# don't do anything; we are already active for this transaction
|
99
|
-
else
|
100
|
-
block.call
|
101
|
-
execute_after_commit { @_counter_culture_active = false}
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
92
|
# called by after_create callback
|
106
93
|
def _update_counts_after_create
|
107
|
-
|
108
|
-
|
109
|
-
self
|
110
|
-
# increment counter cache
|
111
|
-
counter.change_counter_cache(self, :increment => true)
|
112
|
-
end
|
94
|
+
self.class.after_commit_counter_cache.each do |counter|
|
95
|
+
# increment counter cache
|
96
|
+
counter.change_counter_cache(self, :increment => true)
|
113
97
|
end
|
114
98
|
end
|
115
99
|
|
116
100
|
# called by after_destroy callback
|
117
101
|
def _update_counts_after_destroy
|
118
|
-
|
119
|
-
|
120
|
-
self
|
121
|
-
# decrement counter cache
|
122
|
-
counter.change_counter_cache(self, :increment => false)
|
123
|
-
end
|
102
|
+
self.class.after_commit_counter_cache.each do |counter|
|
103
|
+
# decrement counter cache
|
104
|
+
counter.change_counter_cache(self, :increment => false)
|
124
105
|
end
|
125
106
|
end
|
126
107
|
|
127
108
|
# called by after_update callback
|
128
109
|
def _update_counts_after_update
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
counter.change_counter_cache(self, :increment => true, :counter_column => counter_cache_name)
|
144
|
-
# decrement the counter cache of the old value
|
145
|
-
counter.change_counter_cache(self, :increment => false, :was => true, :counter_column => counter_cache_name_was)
|
146
|
-
end
|
110
|
+
self.class.after_commit_counter_cache.each do |counter|
|
111
|
+
# figure out whether the applicable counter cache changed (this can happen
|
112
|
+
# with dynamic column names)
|
113
|
+
counter_cache_name_was = counter.counter_cache_name_for(counter.previous_model(self))
|
114
|
+
counter_cache_name = counter.counter_cache_name_for(self)
|
115
|
+
|
116
|
+
if counter.first_level_relation_changed?(self) ||
|
117
|
+
(counter.delta_column && counter.attribute_changed?(self, counter.delta_column)) ||
|
118
|
+
counter_cache_name != counter_cache_name_was
|
119
|
+
|
120
|
+
# increment the counter cache of the new value
|
121
|
+
counter.change_counter_cache(self, :increment => true, :counter_column => counter_cache_name)
|
122
|
+
# decrement the counter cache of the old value
|
123
|
+
counter.change_counter_cache(self, :increment => false, :was => true, :counter_column => counter_cache_name_was)
|
147
124
|
end
|
148
125
|
end
|
149
126
|
end
|
@@ -148,6 +148,34 @@ describe "CounterCulture" do
|
|
148
148
|
expect(user2.reload.review_approvals_count).to eq(69)
|
149
149
|
end
|
150
150
|
|
151
|
+
it "works with multiple saves in one transcation" do
|
152
|
+
user = User.create
|
153
|
+
product = Product.create
|
154
|
+
|
155
|
+
expect(user.reviews_count).to eq(0)
|
156
|
+
expect(user.review_approvals_count).to eq(0)
|
157
|
+
|
158
|
+
Review.transaction do
|
159
|
+
review1 = Review.create!(user_id: user.id, product_id: product.id, approvals: 0)
|
160
|
+
|
161
|
+
user.reload
|
162
|
+
expect(user.reviews_count).to eq(1)
|
163
|
+
expect(user.review_approvals_count).to eq(0)
|
164
|
+
|
165
|
+
review1.update_attributes!(approvals: 42)
|
166
|
+
|
167
|
+
user.reload
|
168
|
+
expect(user.reviews_count).to eq(1)
|
169
|
+
expect(user.review_approvals_count).to eq(42)
|
170
|
+
|
171
|
+
review2 = Review.create!(user_id: user.id, product_id: product.id, approvals: 1)
|
172
|
+
|
173
|
+
user.reload
|
174
|
+
expect(user.reviews_count).to eq(2)
|
175
|
+
expect(user.review_approvals_count).to eq(43)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
151
179
|
it "treats null delta column values as 0" do
|
152
180
|
user = User.create
|
153
181
|
product = Product.create
|
data/spec/models/review.rb
CHANGED
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:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Magnus von Koeller
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-06-
|
11
|
+
date: 2018-06-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: after_commit_action
|