cached_counts 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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