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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e36d2910a85a7b6354c086adad83820f1503947aba48f3caeb3c09b424b565d5
4
- data.tar.gz: 6e8892f7c9a9d447117ff9ae0c7c23f16afc00739e93e4b1fd407631989fa0eb
3
+ metadata.gz: c9162e152f23f2e1bcbe16bc8eac89109fb42e3710f056e280ffc7e2509972ea
4
+ data.tar.gz: 48a92f691a0d60be81222f930ee511cf7cc5af62b90a2d6dbb940d2bd6c40e83
5
5
  SHA512:
6
- metadata.gz: 7986c0f65fd6c43a38a71f4acfd6d62a7bd36e9aeeb03dbe613dff4553d1c0119cd88b48a1c6d19adffd06e4cfd214462202fc8aafe84e8e05d2034288219750
7
- data.tar.gz: f52a1259232d2ae16a2117ea42f0950a41f81379892387c7f4191ec595918750b87b4f9b482223faaac03d19c50f459cd4af610523c89428bd38635c25ac0f41
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
- has_many :counters, as: :countable, dependent: :destroy
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
- # check if the counter is loaded
15
- if counters.loaded? && counter = counters.detect{|c| c.key == cache_column.to_s }
16
- counter.value
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
- if ( counter = counters.find_by(key: cache_column.to_s) )
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
- counters.create key: cache_column.to_s, value: count.to_i
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
- foreign_key = reflection.try(:foreign_key) || reflection.association_foreign_key
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
- target_key = reflection.options[:polymorphic] ? "#{association}_type" : foreign_key
57
- if send("saved_change_to_#{target_key}?")
58
- old_id = send("#{target_key}_before_last_save")
59
- klass = if reflection.options[:polymorphic]
60
- ( old_id || send("#{association}_type") ).constantize
61
- else
62
- reflection.klass
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
- if ( old_id && record = klass.find_by(id: old_id) )
65
- record.send("update_#{cache_column}")
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
@@ -1,5 +1,5 @@
1
1
  module CustomCounterCache
2
2
 
3
- VERSION = '0.3.0'
3
+ VERSION = '0.3.2'
4
4
 
5
5
  end
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.0
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: 2026-04-01 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: rails
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
- - test/counter_test.rb
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
- post_install_message:
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.0'
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: 1.3.6
132
+ version: '0'
76
133
  requirements: []
77
- rubygems_version: 3.5.22
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