custom_counter_cache 0.3.0 → 0.3.2
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 +27 -0
- data/LICENSE +9 -0
- data/README.rdoc +87 -0
- data/lib/custom_counter_cache/model.rb +35 -16
- data/lib/custom_counter_cache/version.rb +1 -1
- metadata +71 -17
- data/test/counter_test.rb +0 -110
- data/test/test_helper.rb +0 -93
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c9162e152f23f2e1bcbe16bc8eac89109fb42e3710f056e280ffc7e2509972ea
|
|
4
|
+
data.tar.gz: 48a92f691a0d60be81222f930ee511cf7cc5af62b90a2d6dbb940d2bd6c40e83
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5140b21c810447e0f8593b57252ef42064cecfc26168a46df39fa2f813f042e54aabb32c602f275c6ead13f30fc4c05b929db0cb2ffbb557367be1a4b628ef9
|
|
7
|
+
data.tar.gz: 8a0101a516d27d8b076d97173fa1733b6d4b53f85877448229c30afaa60ea78bb7059fda11e41602de505148497f79a36622bb394f968340f8947fc788b68725
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.2
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Moving a record between owners now recounts the old owner when `belongs_to` uses a custom
|
|
8
|
+
`primary_key:`.
|
|
9
|
+
- Polymorphic `belongs_to` with custom `foreign_key:`/`foreign_type:` no longer raises
|
|
10
|
+
`NoMethodError` on save.
|
|
11
|
+
- Moving a record away from a polymorphic type whose class no longer exists no longer raises
|
|
12
|
+
`NameError`; the new owner is still recounted.
|
|
13
|
+
- With `includes(:counters)`, counters stay current after an update, and a missing counter
|
|
14
|
+
returns 0 without a query.
|
|
15
|
+
- Two saves creating the same virtual counter at once no longer fail with
|
|
16
|
+
`ActiveRecord::RecordNotUnique`; the second updates the row instead.
|
|
17
|
+
- `update_counter_cache` without its `belongs_to` raises a clear `ArgumentError` instead of
|
|
18
|
+
`NoMethodError ... for nil`.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Depend on `activerecord` and `activesupport` instead of all of `rails`.
|
|
23
|
+
- Require Ruby >= 3.1 in the gemspec, matching the README and Rails 7.2.
|
|
24
|
+
- Test against every non-EOL Ruby (3.3, 3.4, 4.0) and Rails (8.0, 8.1).
|
|
25
|
+
- Expand the test suite to cover all branches.
|
|
26
|
+
- README: fix the `:if` example (`state_changed?` is always false in after callbacks)
|
|
27
|
+
and note that `:if` also applies to `after_destroy`.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright © 2011-2026 Cedric Howe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{rdoc-image:https://github.com/cedric/custom_counter_cache/actions/workflows/ci.yml/badge.svg}[https://github.com/cedric/custom_counter_cache/actions/workflows/ci.yml]
|
|
2
|
+
|
|
3
|
+
== Custom Counter Cache
|
|
4
|
+
|
|
5
|
+
This is a simple approach to creating a custom counter cache in Rails that can be used across multiple models.
|
|
6
|
+
|
|
7
|
+
=== Requirements
|
|
8
|
+
|
|
9
|
+
* Ruby >= 3.1
|
|
10
|
+
* Rails (Active Record) >= 7.2
|
|
11
|
+
|
|
12
|
+
CI covers the Ruby and Rails versions that are not yet end-of-life.
|
|
13
|
+
|
|
14
|
+
=== Installation
|
|
15
|
+
|
|
16
|
+
Add the following to your Gemfile:
|
|
17
|
+
|
|
18
|
+
gem 'custom_counter_cache'
|
|
19
|
+
|
|
20
|
+
=== Example
|
|
21
|
+
|
|
22
|
+
== Class with counter cache
|
|
23
|
+
|
|
24
|
+
This is the block that will be used to calculate the value for the counter cache. It will be called by other models through their association via an after_save or after_destroy callback.
|
|
25
|
+
|
|
26
|
+
include CustomCounterCache::Model
|
|
27
|
+
define_counter_cache :articles_count do |user|
|
|
28
|
+
user.articles.where(state: 'published').count
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
== Class with callbacks
|
|
32
|
+
|
|
33
|
+
This will define the after_create, after_update and after_destroy callbacks. An :if option can be provided to limit when these callbacks get triggered.
|
|
34
|
+
|
|
35
|
+
include CustomCounterCache::Model
|
|
36
|
+
update_counter_cache :user, :articles_count, if: -> (article) { article.destroyed? || article.saved_change_to_state? }
|
|
37
|
+
|
|
38
|
+
The callbacks run after the record is saved, so use <tt>saved_change_to_state?</tt> rather than
|
|
39
|
+
<tt>state_changed?</tt> (which is always false by then). The :if condition also applies to
|
|
40
|
+
after_destroy, where there are no saved changes, hence the <tt>destroyed?</tt> check.
|
|
41
|
+
|
|
42
|
+
Other options: <tt>:unless</tt>, <tt>:only</tt> / <tt>:except</tt> (e.g. <tt>only: [:create, :destroy]</tt>)
|
|
43
|
+
and <tt>:prepend</tt>.
|
|
44
|
+
|
|
45
|
+
These callbacks can be added to any number of models that might need to change the counter cache.
|
|
46
|
+
|
|
47
|
+
== Counter Cache
|
|
48
|
+
|
|
49
|
+
To store the counter cache you need to create a column for the model with the counter cache (example: articles_count).
|
|
50
|
+
|
|
51
|
+
If you would like to store all of your counter caches in a single table, you can use this migration:
|
|
52
|
+
|
|
53
|
+
create_table :counters do |t|
|
|
54
|
+
t.references :countable, polymorphic: true
|
|
55
|
+
t.string :key, null: false
|
|
56
|
+
t.integer :value, null: false, default: 0
|
|
57
|
+
t.timestamps
|
|
58
|
+
end
|
|
59
|
+
add_index :counters, [ :countable_id, :countable_type, :key ], unique: true
|
|
60
|
+
|
|
61
|
+
Here is the example model to go with:
|
|
62
|
+
|
|
63
|
+
class Counter < ActiveRecord::Base
|
|
64
|
+
belongs_to :countable, polymorphic: true
|
|
65
|
+
validates :countable, presence: true
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
Note: do not add <tt>dependent: :destroy</tt> to that +belongs_to+. On a
|
|
69
|
+
+belongs_to+, <tt>dependent: :destroy</tt> means "when this Counter is
|
|
70
|
+
destroyed, also destroy the record it belongs to" -- the opposite of what you
|
|
71
|
+
want, and it creates a destroy loop: destroying a countable record cascades
|
|
72
|
+
into destroying its Counters (via the <tt>has_many :counters, dependent:
|
|
73
|
+
:delete_all</tt> this gem defines), and a wrongly-configured +belongs_to+ would
|
|
74
|
+
then cascade back into destroying the same countable record again. The
|
|
75
|
+
countable side's <tt>dependent: :delete_all</tt> already handles cleanup and
|
|
76
|
+
skips Counter's own callbacks entirely, so no +dependent+ option belongs on
|
|
77
|
+
the +belongs_to+ side at all.
|
|
78
|
+
|
|
79
|
+
If you would like to store your counter cache in an existing table, you can use this migration:
|
|
80
|
+
|
|
81
|
+
def change
|
|
82
|
+
add_column :users, :articles_count, :integer, default: 0, null: false
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
To backfill your counters, run something like this either in a migration or in the console:
|
|
86
|
+
|
|
87
|
+
User.select(:id).find_each(batch_size: 100) { |u| u.update_articles_count }
|
|
@@ -9,20 +9,30 @@ module CustomCounterCache::Model
|
|
|
9
9
|
|
|
10
10
|
# counter accessors
|
|
11
11
|
unless column_names.include?(cache_column.to_s)
|
|
12
|
-
|
|
12
|
+
# Declare once per class: redeclaring for each virtual counter triggers method-redefinition warnings.
|
|
13
|
+
has_many :counters, as: :countable, dependent: :delete_all unless reflect_on_association(:counters)
|
|
13
14
|
define_method "#{cache_column}" do
|
|
14
|
-
#
|
|
15
|
-
if counters.loaded?
|
|
16
|
-
|
|
15
|
+
# Once loaded (e.g. includes(:counters)), a missing row means 0, not a query.
|
|
16
|
+
if counters.loaded?
|
|
17
|
+
counters.detect { |c| c.key == cache_column.to_s }.try(:value).to_i
|
|
17
18
|
else
|
|
18
19
|
counters.find_by(key: cache_column.to_s).try(:value).to_i
|
|
19
20
|
end
|
|
20
21
|
end
|
|
21
22
|
define_method "#{cache_column}=" do |count|
|
|
22
|
-
|
|
23
|
+
# Update the loaded Counter itself, or the reader keeps returning its stale value.
|
|
24
|
+
counter = counters.loaded? ? counters.detect { |c| c.key == cache_column.to_s } : counters.find_by(key: cache_column.to_s)
|
|
25
|
+
if counter
|
|
23
26
|
counter.update_attribute :value, count.to_i
|
|
24
27
|
else
|
|
25
|
-
|
|
28
|
+
begin
|
|
29
|
+
# Savepoint: on PostgreSQL a failed INSERT would otherwise abort the caller's whole transaction.
|
|
30
|
+
self.class.transaction(requires_new: true) { counters.create key: cache_column.to_s, value: count.to_i }
|
|
31
|
+
rescue ActiveRecord::RecordNotUnique
|
|
32
|
+
# Lost a create race. A locking read sees the winner's row even under REPEATABLE READ (MySQL).
|
|
33
|
+
counters.reset
|
|
34
|
+
counters.lock.find_by!(key: cache_column.to_s).update_attribute :value, count.to_i
|
|
35
|
+
end
|
|
26
36
|
end
|
|
27
37
|
end
|
|
28
38
|
end
|
|
@@ -48,21 +58,30 @@ module CustomCounterCache::Model
|
|
|
48
58
|
cache_column = cache_column.to_sym
|
|
49
59
|
method_name = "callback_#{association}_#{cache_column}".to_sym
|
|
50
60
|
reflection = reflect_on_association(association)
|
|
51
|
-
|
|
61
|
+
raise ArgumentError, "#{self} must declare belongs_to :#{association} before update_counter_cache" unless reflection
|
|
62
|
+
foreign_key = reflection.foreign_key
|
|
52
63
|
|
|
53
64
|
# define callback
|
|
54
65
|
define_method method_name do
|
|
55
66
|
# update old association
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
(
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
if reflection.polymorphic?
|
|
68
|
+
type_key = reflection.foreign_type
|
|
69
|
+
id_key = foreign_key
|
|
70
|
+
if saved_change_to_attribute?(id_key) || saved_change_to_attribute?(type_key)
|
|
71
|
+
old_type = saved_change_to_attribute?(type_key) ? attribute_before_last_save(type_key) : self[type_key]
|
|
72
|
+
old_id = saved_change_to_attribute?(id_key) ? attribute_before_last_save(id_key) : self[id_key]
|
|
73
|
+
# safe_: the stored type may name a class that has since been renamed or removed.
|
|
74
|
+
old_klass = old_type&.safe_constantize
|
|
75
|
+
if ( old_klass && old_id && record = old_klass.find_by(reflection.association_primary_key(old_klass) => old_id) )
|
|
76
|
+
record.send("update_#{cache_column}")
|
|
77
|
+
end
|
|
63
78
|
end
|
|
64
|
-
|
|
65
|
-
|
|
79
|
+
else
|
|
80
|
+
if saved_change_to_attribute?(foreign_key)
|
|
81
|
+
old_id = attribute_before_last_save(foreign_key)
|
|
82
|
+
if ( old_id && record = reflection.klass.find_by(reflection.association_primary_key => old_id) )
|
|
83
|
+
record.send("update_#{cache_column}")
|
|
84
|
+
end
|
|
66
85
|
end
|
|
67
86
|
end
|
|
68
87
|
# update new association
|
metadata
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: custom_counter_cache
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cedric Howe
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
13
|
+
name: activerecord
|
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
@@ -30,6 +29,60 @@ dependencies:
|
|
|
30
29
|
- - "<"
|
|
31
30
|
- !ruby/object:Gem::Version
|
|
32
31
|
version: '9.0'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: activesupport
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '7.2'
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '9.0'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '7.2'
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '9.0'
|
|
52
|
+
- !ruby/object:Gem::Dependency
|
|
53
|
+
name: minitest
|
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '5.0'
|
|
59
|
+
- - "<"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '7'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '5.0'
|
|
69
|
+
- - "<"
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '7'
|
|
72
|
+
- !ruby/object:Gem::Dependency
|
|
73
|
+
name: rake
|
|
74
|
+
requirement: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - "~>"
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '13.0'
|
|
79
|
+
type: :development
|
|
80
|
+
prerelease: false
|
|
81
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - "~>"
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: '13.0'
|
|
33
86
|
- !ruby/object:Gem::Dependency
|
|
34
87
|
name: sqlite3
|
|
35
88
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -44,22 +97,26 @@ dependencies:
|
|
|
44
97
|
- - "~>"
|
|
45
98
|
- !ruby/object:Gem::Version
|
|
46
99
|
version: '2.0'
|
|
47
|
-
description:
|
|
100
|
+
description: Define counter caches computed by an arbitrary block (e.g. a scoped count),
|
|
101
|
+
refreshed by callbacks on any number of associated models, and stored in a column
|
|
102
|
+
or a shared polymorphic counters table.
|
|
48
103
|
email: cedric@howe.net
|
|
49
104
|
executables: []
|
|
50
105
|
extensions: []
|
|
51
106
|
extra_rdoc_files: []
|
|
52
107
|
files:
|
|
108
|
+
- CHANGELOG.md
|
|
109
|
+
- LICENSE
|
|
110
|
+
- README.rdoc
|
|
53
111
|
- lib/custom_counter_cache.rb
|
|
54
112
|
- lib/custom_counter_cache/model.rb
|
|
55
113
|
- lib/custom_counter_cache/version.rb
|
|
56
|
-
|
|
57
|
-
- test/test_helper.rb
|
|
58
|
-
homepage: http://github.com/cedric/custom_counter_cache/
|
|
114
|
+
homepage: https://github.com/cedric/custom_counter_cache
|
|
59
115
|
licenses:
|
|
60
116
|
- MIT
|
|
61
|
-
metadata:
|
|
62
|
-
|
|
117
|
+
metadata:
|
|
118
|
+
source_code_uri: https://github.com/cedric/custom_counter_cache
|
|
119
|
+
changelog_uri: https://github.com/cedric/custom_counter_cache/blob/main/CHANGELOG.md
|
|
63
120
|
rdoc_options: []
|
|
64
121
|
require_paths:
|
|
65
122
|
- lib
|
|
@@ -67,18 +124,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
67
124
|
requirements:
|
|
68
125
|
- - ">="
|
|
69
126
|
- !ruby/object:Gem::Version
|
|
70
|
-
version: '3.
|
|
127
|
+
version: '3.1'
|
|
71
128
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
129
|
requirements:
|
|
73
130
|
- - ">="
|
|
74
131
|
- !ruby/object:Gem::Version
|
|
75
|
-
version:
|
|
132
|
+
version: '0'
|
|
76
133
|
requirements: []
|
|
77
|
-
rubygems_version:
|
|
78
|
-
signing_key:
|
|
134
|
+
rubygems_version: 4.0.21
|
|
79
135
|
specification_version: 4
|
|
80
136
|
summary: Custom counter_cache functionality that supports conditions and multiple
|
|
81
137
|
models.
|
|
82
|
-
test_files:
|
|
83
|
-
- test/counter_test.rb
|
|
84
|
-
- test/test_helper.rb
|
|
138
|
+
test_files: []
|
data/test/counter_test.rb
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
require File.join(File.dirname(__FILE__), 'test_helper')
|
|
2
|
-
|
|
3
|
-
class CounterTest < Minitest::Test
|
|
4
|
-
|
|
5
|
-
def setup
|
|
6
|
-
@user = User.create
|
|
7
|
-
@box = Box.create
|
|
8
|
-
Counter.destroy_all
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def test_default_counter_value
|
|
12
|
-
assert_equal 0, @user.published_count
|
|
13
|
-
assert_equal 0, @box.green_balls_count
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def test_create_and_destroy_counter
|
|
17
|
-
@user.articles.create(state: 'published')
|
|
18
|
-
assert_equal 1, Counter.count
|
|
19
|
-
@user.destroy
|
|
20
|
-
assert_equal 0, Counter.count
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def test_create_and_destroy_polymorphic_association_counter
|
|
24
|
-
@article = @user.articles.create(state: "published")
|
|
25
|
-
assert_equal 0, @article.comments.size
|
|
26
|
-
@comment = @article.comments.create(state: "published")
|
|
27
|
-
assert_equal 1, @article.comments.size
|
|
28
|
-
@article.destroy
|
|
29
|
-
assert_equal 0, @article.comments.size
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def test_increment_and_decrement_counter_with_conditions
|
|
33
|
-
@article = @user.articles.create(state: 'unpublished')
|
|
34
|
-
assert_equal 0, @user.published_count
|
|
35
|
-
@article.update_attribute :state, 'published'
|
|
36
|
-
assert_equal 1, @user.published_count
|
|
37
|
-
3.times { |i| @user.articles.create(state: 'published') }
|
|
38
|
-
assert_equal 4, @user.published_count
|
|
39
|
-
@user.articles.each {|a| a.update(state: 'unpublished') }
|
|
40
|
-
assert_equal 0, @user.published_count
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def test_increment_and_decrement_polymorphic_counter_with_conditions
|
|
44
|
-
@article = @user.articles.create(state: "published")
|
|
45
|
-
@comment = @article.comments.create(state: "unpublished")
|
|
46
|
-
assert_equal 0, @article.comments_count
|
|
47
|
-
@comment.update_attribute :state, "published"
|
|
48
|
-
assert_equal 1, @article.comments_count
|
|
49
|
-
3.times { |i| @article.comments.create(state: "published") }
|
|
50
|
-
assert_equal 4, @article.comments_count
|
|
51
|
-
@article.comments.each { |c| c.update(state: 'unpublished') }
|
|
52
|
-
assert_equal 0, @article.comments_count
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def test_increment_and_decrement_counter_with_conditions_on_model_with_counter_column
|
|
56
|
-
@ball = @box.balls.create(color: 'red')
|
|
57
|
-
assert_equal 0, @box.reload.green_balls_count
|
|
58
|
-
@ball.update_attribute :color, 'green'
|
|
59
|
-
assert_equal 1, @box.reload.green_balls_count
|
|
60
|
-
3.times { |i| @box.balls.create(color: 'green') }
|
|
61
|
-
assert_equal 4, @box.reload.green_balls_count
|
|
62
|
-
@box.balls.each {|b| b.update(color: 'red') }
|
|
63
|
-
assert_equal 0, @box.reload.green_balls_count
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Test that an eager loaded
|
|
67
|
-
def test_eager_loading_with_no_counter
|
|
68
|
-
@article = @user.articles.create(state: 'unpublished')
|
|
69
|
-
user = User.includes(:counters).first
|
|
70
|
-
assert_equal 0, user.published_count
|
|
71
|
-
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def test_eager_loading_with_counter
|
|
75
|
-
@article = @user.articles.create(state: 'published')
|
|
76
|
-
@user = User.includes(:counters).find(@user.id)
|
|
77
|
-
assert_equal 1, @user.published_count
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def test_except_option
|
|
81
|
-
@ball = @box.balls.create
|
|
82
|
-
assert_equal 1, @box.reload.lifetime_balls_count
|
|
83
|
-
@ball.update(color: 'green')
|
|
84
|
-
assert_equal 1, @box.reload.lifetime_balls_count
|
|
85
|
-
@ball.destroy
|
|
86
|
-
assert_equal 1, @box.reload.lifetime_balls_count
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def test_only_option
|
|
90
|
-
@ball = @box.balls.create
|
|
91
|
-
assert_equal 0, @box.reload.destroyed_balls_count
|
|
92
|
-
@ball.update(color: 'green')
|
|
93
|
-
assert_equal 0, @box.reload.destroyed_balls_count
|
|
94
|
-
@ball.destroy
|
|
95
|
-
assert_equal 1, @box.reload.destroyed_balls_count
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def test_reassigning_article_to_different_user_updates_both_counters
|
|
99
|
-
@user2 = User.create
|
|
100
|
-
@article = @user.articles.create(state: 'unpublished')
|
|
101
|
-
assert_equal 0, @user.reload.published_count
|
|
102
|
-
assert_equal 0, @user2.reload.published_count
|
|
103
|
-
# Changing both user and state triggers the :if condition (state changed)
|
|
104
|
-
# and the reassignment logic inside the callback updates the old user's counter too
|
|
105
|
-
@article.update(user: @user2, state: 'published')
|
|
106
|
-
assert_equal 0, @user.reload.published_count
|
|
107
|
-
assert_equal 1, @user2.reload.published_count
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
end
|
data/test/test_helper.rb
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
require 'rubygems'
|
|
2
|
-
require 'minitest/autorun'
|
|
3
|
-
require 'sqlite3'
|
|
4
|
-
require 'action_view'
|
|
5
|
-
require 'active_record'
|
|
6
|
-
require 'custom_counter_cache'
|
|
7
|
-
|
|
8
|
-
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
|
9
|
-
|
|
10
|
-
ActiveRecord::Schema.define(version: 1) do
|
|
11
|
-
create_table :users do |t|
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
create_table :articles do |t|
|
|
15
|
-
t.belongs_to :user
|
|
16
|
-
t.string :state, default: 'unpublished'
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
create_table :comments do |t|
|
|
20
|
-
t.belongs_to :user
|
|
21
|
-
t.references :commentable, polymorphic: true
|
|
22
|
-
t.string :state, default: "unpublished"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
create_table :counters do |t|
|
|
26
|
-
t.references :countable, polymorphic: true
|
|
27
|
-
t.string :key, null: false
|
|
28
|
-
t.integer :value, null: false, default: 0
|
|
29
|
-
end
|
|
30
|
-
add_index :counters, [ :countable_id, :countable_type, :key ], unique: true
|
|
31
|
-
|
|
32
|
-
create_table :boxes do |t|
|
|
33
|
-
t.integer :green_balls_count, default: 0
|
|
34
|
-
t.integer :lifetime_balls_count, default: 0
|
|
35
|
-
t.integer :destroyed_balls_count, default: 0
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
create_table :balls do |t|
|
|
39
|
-
t.belongs_to :box
|
|
40
|
-
t.string :color, default: 'red'
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
class ApplicationRecord < ActiveRecord::Base
|
|
45
|
-
self.abstract_class = true
|
|
46
|
-
include CustomCounterCache::Model
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
class User < ApplicationRecord
|
|
50
|
-
has_many :articles, dependent: :destroy
|
|
51
|
-
define_counter_cache :published_count do |user|
|
|
52
|
-
user.articles.where(articles: { state: 'published' }).count
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
class Article < ApplicationRecord
|
|
57
|
-
belongs_to :user
|
|
58
|
-
update_counter_cache :user, :published_count, if: Proc.new { |article| article.saved_change_to_attribute?(:state) }
|
|
59
|
-
has_many :comments, as: :commentable, dependent: :destroy
|
|
60
|
-
define_counter_cache :comments_count do |article|
|
|
61
|
-
article.comments.where(state: "published").count
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
class Comment < ApplicationRecord
|
|
66
|
-
belongs_to :commentable, polymorphic: true
|
|
67
|
-
update_counter_cache :commentable, :comments_count, if: Proc.new { |comment| comment.saved_change_to_attribute?(:state) }
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
class Counter < ApplicationRecord
|
|
71
|
-
belongs_to :countable, polymorphic: true
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
class Box < ApplicationRecord
|
|
75
|
-
has_many :balls
|
|
76
|
-
define_counter_cache :green_balls_count do |box|
|
|
77
|
-
box.balls.green.count
|
|
78
|
-
end
|
|
79
|
-
define_counter_cache :lifetime_balls_count do |box|
|
|
80
|
-
box.lifetime_balls_count + 1
|
|
81
|
-
end
|
|
82
|
-
define_counter_cache :destroyed_balls_count do |box|
|
|
83
|
-
box.destroyed_balls_count + 1
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
class Ball < ApplicationRecord
|
|
88
|
-
belongs_to :box
|
|
89
|
-
scope :green, lambda { where(color: 'green') }
|
|
90
|
-
update_counter_cache :box, :green_balls_count, if: Proc.new { |ball| ball.saved_change_to_attribute?(:color) }
|
|
91
|
-
update_counter_cache :box, :lifetime_balls_count, except: [:update, :destroy]
|
|
92
|
-
update_counter_cache :box, :destroyed_balls_count, only: [:destroy]
|
|
93
|
-
end
|