cached_counts 0.1.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.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +82 -0
- data/Rakefile +1 -0
- data/cached_counts.gemspec +25 -0
- data/contributing.md +8 -0
- data/lib/cached_counts/active_record_base_methods.rb +11 -0
- data/lib/cached_counts/active_record_relation_methods.rb +29 -0
- data/lib/cached_counts/cache.rb +37 -0
- data/lib/cached_counts/cached_count_observer.rb +21 -0
- data/lib/cached_counts/railtie.rb +14 -0
- data/lib/cached_counts/version.rb +3 -0
- data/lib/cached_counts.rb +15 -0
- data/spec/cached_counts/cache_spec.rb +69 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/support/cache.rb +11 -0
- data/spec/support/rails.rb +5 -0
- data/spec/support/scope.rb +7 -0
- metadata +137 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use @cached_counts --create
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Mario Visic
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# Cached Counts
|
2
|
+
|
3
|
+
Performing `COUNT(*)` operations in relational databases can get slow for large sets of data. There are already existing ways to deal with improving counting performance and techniques to avoid doing it altogether.
|
4
|
+
|
5
|
+
This gem adds caching support to the `ActiveRecord::Relation` class to allow repeated count calls to be cached. Counts are cached based on the query that is to be run and cleared when records are saved or destroyed.
|
6
|
+
|
7
|
+
Cached counts works well with tables that are large and have many more selects than inserts. You will see *some* benefits even in tables that have regular inserts, but the gains will not be as great.
|
8
|
+
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add it to your gemfile in your Rails 3.2+ project
|
13
|
+
|
14
|
+
gem 'cached_counts'
|
15
|
+
|
16
|
+
Cached counts uses your Rails.cache setup, for a good memcached store, [look at dalli](https://github.com/mperham/dalli)
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
You can use the size, length or count method on any active record model.
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
User.count # => # Cached count value
|
24
|
+
User.size # => # Cached count value
|
25
|
+
User.length # => # Cached count value
|
26
|
+
```
|
27
|
+
|
28
|
+
You can use scopes as well
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
User.where(:admin => true).count # Cached count value for this specific query
|
32
|
+
```
|
33
|
+
|
34
|
+
You can clear the cache at any time for a model:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
User.clear_count_cache
|
38
|
+
```
|
39
|
+
|
40
|
+
You can also use the non cached count on the class or scopes.
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
User.count_without_caching # => Runs a database lookup
|
44
|
+
User.where(:admin => true).count_without_caching # => Runs a database lookup
|
45
|
+
```
|
46
|
+
|
47
|
+
## Clearing the cache
|
48
|
+
|
49
|
+
The counts cache is cleared for a model after save and after destroy.
|
50
|
+
If you are updating the database manually without these methods then you will need to clear the cache yourself.
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
User.update_all(:name => 'joe')
|
54
|
+
User.clear_count_cache
|
55
|
+
```
|
56
|
+
|
57
|
+
## Example
|
58
|
+
|
59
|
+
```
|
60
|
+
1.9.3p194 :020 > User.count
|
61
|
+
(0.2ms) SELECT COUNT(*) FROM "users"
|
62
|
+
=> 130
|
63
|
+
1.9.3p194 :021 > User.count
|
64
|
+
=> 130
|
65
|
+
1.9.3p194 :022 > User.last.destroy
|
66
|
+
# ...
|
67
|
+
=> #<User id: 131 ...>
|
68
|
+
1.9.3p194 :023 > User.count
|
69
|
+
(0.4ms) SELECT COUNT(*) FROM "users"
|
70
|
+
=> 129
|
71
|
+
1.9.3p194 :024 > User.count
|
72
|
+
=> 129
|
73
|
+
1.9.3p194 :036 > User.where(:admin => true).count
|
74
|
+
(0.5ms) SELECT COUNT(*) FROM "users" WHERE "users"."admin" = 't'
|
75
|
+
=> 72
|
76
|
+
1.9.3p194 :037 > User.where(:admin => true).count
|
77
|
+
=> 72
|
78
|
+
1.9.3p194 :038 >
|
79
|
+
```
|
80
|
+
|
81
|
+
## TODO
|
82
|
+
- Better testing
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'cached_counts/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "cached_counts"
|
8
|
+
gem.version = CachedCounts::VERSION
|
9
|
+
gem.authors = ["Mario Visic"]
|
10
|
+
gem.email = ["mario@mariovisic.com"]
|
11
|
+
gem.description = %q{Speed up ActiveRecord counting with caching}
|
12
|
+
gem.summary = %q{Uses the Rails cache to store cached counts for active record models as well as models with scopes}
|
13
|
+
gem.homepage = "https://github.com/desktoppr/cached_counts"
|
14
|
+
gem.license = "MIT"
|
15
|
+
|
16
|
+
gem.files = `git ls-files`.split($/)
|
17
|
+
gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
19
|
+
gem.require_paths = ["lib"]
|
20
|
+
|
21
|
+
gem.add_development_dependency "rake"
|
22
|
+
gem.add_development_dependency "rspec"
|
23
|
+
gem.add_development_dependency "activesupport"
|
24
|
+
gem.add_development_dependency "activerecord"
|
25
|
+
end
|
data/contributing.md
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
## Contributing
|
2
|
+
|
3
|
+
1. Fork it
|
4
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
5
|
+
3. Write a failing test
|
6
|
+
4. Commit your changes with passing tests (`git commit -am 'Add some feature'`)
|
7
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
8
|
+
6. Create new Pull Request
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
module CachedCounts
|
4
|
+
module ActiveRecordBaseMethods
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
delegate :clear_count_cache, :count_with_caching, :length_with_caching,
|
8
|
+
:size_with_caching, :count_without_caching, :length_without_caching,
|
9
|
+
:size_without_caching, :to => :scoped
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
module CachedCounts
|
4
|
+
module ActiveRecordRelationMethods
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
alias_method_chain :count, :caching
|
9
|
+
alias_method_chain :length, :caching
|
10
|
+
alias_method_chain :size, :caching
|
11
|
+
end
|
12
|
+
|
13
|
+
def count_with_caching(*args)
|
14
|
+
CachedCounts::Cache.new(self).count
|
15
|
+
end
|
16
|
+
|
17
|
+
def length_with_caching(*args)
|
18
|
+
CachedCounts::Cache.new(self).count
|
19
|
+
end
|
20
|
+
|
21
|
+
def size_with_caching(*args)
|
22
|
+
CachedCounts::Cache.new(self).count
|
23
|
+
end
|
24
|
+
|
25
|
+
def clear_count_cache
|
26
|
+
CachedCounts::Cache.new(self).clear
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module CachedCounts
|
2
|
+
class Cache
|
3
|
+
def initialize(scope)
|
4
|
+
@scope = scope
|
5
|
+
end
|
6
|
+
|
7
|
+
def count(*args)
|
8
|
+
if all_keys.include?(current_key)
|
9
|
+
Rails.cache.fetch(current_key)
|
10
|
+
else
|
11
|
+
@scope.count_without_caching(*args).tap do |count|
|
12
|
+
Rails.cache.write(current_key, count)
|
13
|
+
Rails.cache.write(list_key, all_keys + [current_key])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def clear
|
19
|
+
all_keys.each { |key| Rails.cache.delete(key) }
|
20
|
+
Rails.cache.delete(list_key)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def all_keys
|
26
|
+
Rails.cache.fetch(list_key) || []
|
27
|
+
end
|
28
|
+
|
29
|
+
def list_key
|
30
|
+
"count:#{@scope.model_name.underscore}::keys"
|
31
|
+
end
|
32
|
+
|
33
|
+
def current_key
|
34
|
+
"count:#{@scope.model_name.underscore}::cached_count::#{@scope.to_sql}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module CachedCounts
|
4
|
+
class CachedCountObserver < ActiveRecord::Observer
|
5
|
+
observe :'ActiveRecord::Base'
|
6
|
+
|
7
|
+
def reload
|
8
|
+
observed_classes.each do |klass|
|
9
|
+
klass.name.constantize.add_observer(self)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def after_save(record)
|
14
|
+
CachedCounts::Cache.new(record.class).clear
|
15
|
+
end
|
16
|
+
|
17
|
+
def after_destroy(record)
|
18
|
+
CachedCounts::Cache.new(record.class).clear
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module CachedCounts
|
2
|
+
class Railtie < ::Rails::Railtie
|
3
|
+
initializer 'cached_counts' do |app|
|
4
|
+
ActiveSupport.on_load(:active_record) do
|
5
|
+
::ActiveRecord::Relation.send :include, CachedCounts::ActiveRecordRelationMethods
|
6
|
+
::ActiveRecord::Base.send :extend, CachedCounts::ActiveRecordBaseMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
app.config.after_initialize { app.config.active_record.observers += [ :cached_count_observer ] }
|
10
|
+
app.config.to_prepare { CachedCounts::CachedCountObserver.instance.reload }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
begin
|
2
|
+
require 'rails'
|
3
|
+
require 'cached_counts/railtie'
|
4
|
+
rescue LoadError
|
5
|
+
#do nothing
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'cached_counts/Active_record_base_methods'
|
9
|
+
require 'cached_counts/active_record_relation_methods'
|
10
|
+
require 'cached_counts/cache'
|
11
|
+
require 'cached_counts/cached_count_observer'
|
12
|
+
require 'cached_counts/version'
|
13
|
+
|
14
|
+
module CachedCounts
|
15
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'cached_counts/cache'
|
3
|
+
|
4
|
+
describe CachedCounts::Cache do
|
5
|
+
let(:scope) { Scope.new('Cat') }
|
6
|
+
let(:cache) { described_class.new(scope) }
|
7
|
+
|
8
|
+
before { cache.clear }
|
9
|
+
|
10
|
+
describe '#count' do
|
11
|
+
context 'when there is no cached count' do
|
12
|
+
it 'returns the cached count' do
|
13
|
+
scope.should_receive(:count_without_caching).and_return(10)
|
14
|
+
cache.count.should == 10
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'passes any given options to the count method' do
|
18
|
+
scope.should_receive(:count_without_caching).with('first').and_return(5)
|
19
|
+
cache.count('first').should == 5
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'when there is already a cached count' do
|
24
|
+
before do
|
25
|
+
scope.should_receive(:count_without_caching).once.and_return(8)
|
26
|
+
cache.count
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'returns the cached value' do
|
30
|
+
cache.count.should == 8
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context 'using multiple scope objects' do
|
35
|
+
let(:new_scope) { Scope.new('Cat') }
|
36
|
+
let(:same_scope) { Scope.new('Cat') }
|
37
|
+
|
38
|
+
before { new_scope.stub(:to_sql => 'new-sql-here') }
|
39
|
+
|
40
|
+
it 'uses a different cache for different to_sql values' do
|
41
|
+
scope.stub(:count_without_caching => 5)
|
42
|
+
new_scope.stub(:count_without_caching => 4)
|
43
|
+
|
44
|
+
cache.count
|
45
|
+
|
46
|
+
described_class.new(new_scope).count.should == 4
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'uses the same cache for scopes with the same to_sql values' do
|
50
|
+
scope.stub(:count_without_caching => 5)
|
51
|
+
same_scope.should_not_receive(:count_without_caching)
|
52
|
+
|
53
|
+
cache.count
|
54
|
+
described_class.new(same_scope).count.should == 5
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe '#clear' do
|
60
|
+
it 'clears the cached count' do
|
61
|
+
scope.stub(:count_without_caching => 9)
|
62
|
+
cache.count.should == 9
|
63
|
+
cache.clear
|
64
|
+
scope.stub(:count_without_caching => 11)
|
65
|
+
|
66
|
+
cache.count.should == 11
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cached_counts
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Mario Visic
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-02-17 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: activesupport
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: activerecord
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
description: Speed up ActiveRecord counting with caching
|
79
|
+
email:
|
80
|
+
- mario@mariovisic.com
|
81
|
+
executables: []
|
82
|
+
extensions: []
|
83
|
+
extra_rdoc_files: []
|
84
|
+
files:
|
85
|
+
- .gitignore
|
86
|
+
- .rspec
|
87
|
+
- .rvmrc
|
88
|
+
- Gemfile
|
89
|
+
- LICENSE.txt
|
90
|
+
- README.md
|
91
|
+
- Rakefile
|
92
|
+
- cached_counts.gemspec
|
93
|
+
- contributing.md
|
94
|
+
- lib/cached_counts.rb
|
95
|
+
- lib/cached_counts/active_record_base_methods.rb
|
96
|
+
- lib/cached_counts/active_record_relation_methods.rb
|
97
|
+
- lib/cached_counts/cache.rb
|
98
|
+
- lib/cached_counts/cached_count_observer.rb
|
99
|
+
- lib/cached_counts/railtie.rb
|
100
|
+
- lib/cached_counts/version.rb
|
101
|
+
- spec/cached_counts/cache_spec.rb
|
102
|
+
- spec/spec_helper.rb
|
103
|
+
- spec/support/cache.rb
|
104
|
+
- spec/support/rails.rb
|
105
|
+
- spec/support/scope.rb
|
106
|
+
homepage: https://github.com/desktoppr/cached_counts
|
107
|
+
licenses:
|
108
|
+
- MIT
|
109
|
+
post_install_message:
|
110
|
+
rdoc_options: []
|
111
|
+
require_paths:
|
112
|
+
- lib
|
113
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ! '>='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
|
+
none: false
|
121
|
+
requirements:
|
122
|
+
- - ! '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
requirements: []
|
126
|
+
rubyforge_project:
|
127
|
+
rubygems_version: 1.8.24
|
128
|
+
signing_key:
|
129
|
+
specification_version: 3
|
130
|
+
summary: Uses the Rails cache to store cached counts for active record models as well
|
131
|
+
as models with scopes
|
132
|
+
test_files:
|
133
|
+
- spec/cached_counts/cache_spec.rb
|
134
|
+
- spec/spec_helper.rb
|
135
|
+
- spec/support/cache.rb
|
136
|
+
- spec/support/rails.rb
|
137
|
+
- spec/support/scope.rb
|