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 ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use @cached_counts --create
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
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,3 @@
1
+ module CachedCounts
2
+ VERSION = '0.1.0'
3
+ end
@@ -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
@@ -0,0 +1,7 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
3
+
4
+ require 'active_support/all'
5
+ require 'support/cache'
6
+ require 'support/rails'
7
+ require 'support/scope'
@@ -0,0 +1,11 @@
1
+ class Cache < Hash
2
+ # A regular hash will raise a KeyError when we try to fetch a key that does
3
+ # not exist
4
+ def fetch(*args)
5
+ super rescue nil
6
+ end
7
+
8
+ def write(key, value)
9
+ self[key] = value
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ class Rails;
2
+ def self.cache;
3
+ @@cache ||= Cache.new
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ class Scope
2
+ attr_accessor :model_name, :to_sql
3
+ def initialize(model_name)
4
+ @model_name = model_name
5
+ @to_sql = 'sql'
6
+ end
7
+ end
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