scope_cache_key 0.0.1

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.
@@ -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
+ --color
2
+ --format progress
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 1.9.3
5
+ - jruby-19mode # JRuby in 1.9 mode
6
+ - rbx-19mode
7
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in scope_cache_key.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Carl Mercier
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.
@@ -0,0 +1,40 @@
1
+ # Scope Cache Key
2
+
3
+ This gem adds the `cache_key` functionality to ActiveRecord scopes.
4
+
5
+ This is useful when caching a page that contains multiple records. It is now
6
+ possible to cache the entire page based on a scope. If any record in the scope
7
+ changes, the entire cache will be busted.
8
+
9
+ This should likely be used in a Russian Doll Caching scenario.
10
+
11
+ NOTE: This currently only works with Postgresql. Adding support for MySQL should
12
+ be trivial.
13
+
14
+ [![Build Status](https://secure.travis-ci.org/cmer/scope_cache_key.png?branch=master)](http://travis-ci.org/cmer/scope_cache_key)
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ gem 'scope_cache_key'
21
+
22
+ ## Usage
23
+
24
+ Example:
25
+
26
+ <%
27
+ scope = Article.page(1) # This should probably be done in your controller!
28
+ cache scope do
29
+ # logic to render and cache each article here
30
+ end
31
+ %>
32
+
33
+
34
+ ## Contributing
35
+
36
+ 1. Fork it
37
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
38
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
39
+ 4. Push to the branch (`git push origin my-new-feature`)
40
+ 5. Create new Pull Request
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,37 @@
1
+ require "scope_cache_key/version"
2
+
3
+ # Add support for passing models and scopes as cache keys.
4
+ # The cache key will include the md5 digest of the ids and
5
+ # timestamps. Any modification to the group of records will
6
+ # generate a new key.
7
+ #
8
+ # Eg.:
9
+ #
10
+ # cache [ Community.first, Category.active ] do ...
11
+ #
12
+ # Will use the key: communites/1/categories/0b27dac757428d88c0f3a0298eb0278f
13
+ module ScopeCacheKey
14
+ # Compute the cache key of a group of records.
15
+ #
16
+ # Item.cache_key # => "0b27dac757428d88c0f3a0298eb0278f"
17
+ # Item.active.cache_key # => "0b27dac757428d88c0f3a0298eb0278e"
18
+ #
19
+ def cache_key
20
+ scope_sql = scoped.select("#{table_name}.id, #{table_name}.updated_at").to_sql
21
+
22
+ sql = "SELECT md5(array_agg(id || '-' || updated_at)::text) " +
23
+ "FROM (#{scope_sql}) as query"
24
+
25
+ md5 = connection.select_value(sql)
26
+
27
+ key = if md5.present?
28
+ md5
29
+ else
30
+ "empty"
31
+ end
32
+
33
+ "#{model_name.cache_key}/#{key}"
34
+ end
35
+ end
36
+
37
+ ActiveRecord::Base.extend ScopeCacheKey
@@ -0,0 +1,3 @@
1
+ module ScopeCacheKey
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'scope_cache_key/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "scope_cache_key"
8
+ gem.version = ScopeCacheKey::VERSION
9
+ gem.authors = ["Carl Mercier"]
10
+ gem.email = ["carl@carlmercier.com"]
11
+ gem.summary = %q{Add cache_key functionality to scopes}
12
+ gem.homepage = "http://github.com/cmer/scope_cache_key"
13
+
14
+ gem.files = `git ls-files`.split($/)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = ["lib"]
18
+
19
+ gem.add_dependency('activerecord', '>= 3.2.9')
20
+
21
+ gem.add_development_dependency('rspec', '~> 2.13.0')
22
+ gem.add_development_dependency('activerecord', '~> 3.2.12')
23
+ gem.add_development_dependency('actionpack', '~> 3.2.12')
24
+ gem.add_development_dependency('pg')
25
+ gem.add_development_dependency('faker', '~> 1.1.2')
26
+ gem.add_development_dependency('pry')
27
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+
3
+ describe ScopeCacheKey do
4
+ before(:all) do
5
+ @article = create_article
6
+ create_comment(Article.first, 100)
7
+ end
8
+
9
+ after(:all) do
10
+ Article.destroy_all
11
+ Comment.destroy_all
12
+ end
13
+
14
+ context "#cache_key" do
15
+ context "model" do
16
+ it "returns the correct value" do
17
+ scope = Comment
18
+ scope.cache_key.should == "comments/#{md5(scope)}"
19
+ end
20
+ end
21
+
22
+ context "after a record is updated" do
23
+ it "returns the correct value" do
24
+ old_key = Comment.cache_key
25
+ Comment.first.touch
26
+ Comment.cache_key.should_not == old_key
27
+ end
28
+ end
29
+
30
+ context "after a record is deleted" do
31
+ it "returns the correct value" do
32
+ old_key = Comment.cache_key
33
+ Comment.first.destroy
34
+ Comment.cache_key.should_not == old_key
35
+ end
36
+ end
37
+
38
+ context "scope returns an empty dataset" do
39
+ it "returns the correct value" do
40
+ Comment.where("1=2").cache_key.should == "comments/empty"
41
+ end
42
+ end
43
+
44
+ context "when order is specified" do
45
+ it "returns the correct value" do
46
+ scope = Comment.reorder(:id)
47
+ scope.cache_key.should == "comments/#{md5(scope)}"
48
+ end
49
+ end
50
+
51
+ context "when joined with another table" do
52
+ it "returns the correct value" do
53
+ Comment.joins(:article).cache_key.should_not == "comments/empty"
54
+ end
55
+ end
56
+
57
+ context "when included with another table" do
58
+ it "returns the correct value" do
59
+ Comment.includes(:article).cache_key.should_not == "comments/empty"
60
+ end
61
+ end
62
+
63
+ context "when offset is specified" do
64
+ it "returns the correct value" do
65
+ Comment.order(:id).offset(1).limit(1).cache_key.should_not == "comments/empty"
66
+ end
67
+ end
68
+ end
69
+
70
+
71
+ context "Fragment Cache Key" do
72
+ let(:controller) { ApplicationController.new }
73
+
74
+ it "returns the correct value when passed a simple model" do
75
+ controller.fragment_cache_key(Comment).should end_with(Comment.cache_key)
76
+ end
77
+
78
+ it "returns the correct value when passed a version and model" do
79
+ controller.fragment_cache_key(['v1', Comment]).should end_with("v1/#{Comment.cache_key}")
80
+ end
81
+
82
+ it "returns the correct value when passed complex arguments" do
83
+ objects = ['v1', Comment, Comment.where('1=2')]
84
+ controller.fragment_cache_key(objects).should end_with(
85
+ "v1/#{Comment.cache_key}/comments/empty")
86
+ end
87
+ end
88
+
89
+ context "performance" do
90
+ before :all do
91
+ @query_time = Benchmark.realtime { Comment.all }
92
+ @cache_key_time = Benchmark.realtime { Comment.cache_key }
93
+ @ruby_time = Benchmark.realtime { md5(Comment) }
94
+ end
95
+
96
+ it "is faster than running the query" do
97
+ @cache_key_time.should < @query_time
98
+ end
99
+
100
+ it "is faster than computing MD5 in Ruby" do
101
+ @cache_key_time.should < @ruby_time
102
+ end
103
+ end
104
+
105
+ def md5(scope)
106
+ string = scope.all.map { |i| "\"#{i.id}-#{i.updated_at_before_type_cast}\"" }.join(",")
107
+ Digest::MD5.hexdigest "{#{string}}"
108
+ end
109
+ end
@@ -0,0 +1,71 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'active_record'
5
+ require 'action_controller'
6
+ require 'faker'
7
+ require 'rspec'
8
+ require 'pry'
9
+ require 'logger'
10
+
11
+ require 'scope_cache_key'
12
+
13
+ RSpec.configure do |config|
14
+ config.treat_symbols_as_metadata_keys_with_true_values = true
15
+ config.run_all_when_everything_filtered = true
16
+ config.filter_run :focus
17
+
18
+ # Run specs in random order to surface order dependencies. If you find an
19
+ # order dependency and want to debug it, you can fix the order by providing
20
+ # the seed, which is printed after each run.
21
+ # --seed 1234
22
+ config.order = 'random'
23
+ end
24
+
25
+ ActiveRecord::Base.configurations = {'postgresql' => {:adapter => 'postgresql', :database => 'scope_cache_key_test'}}
26
+ ActiveRecord::Base.establish_connection('postgresql')
27
+
28
+ ActiveRecord::Base.logger = Logger.new(STDERR)
29
+ ActiveRecord::Base.logger.level = Logger::WARN
30
+
31
+ ActiveRecord::Migration.verbose = false
32
+ ActiveRecord::Base.connection.execute("DROP TABLE articles;")
33
+ ActiveRecord::Base.connection.execute("DROP TABLE comments;")
34
+
35
+ ActiveRecord::Schema.define(:version => 0) do
36
+ create_table :articles do |t|
37
+ t.string :title
38
+ t.text :body
39
+ t.timestamps
40
+ end
41
+
42
+ create_table :comments do |t|
43
+ t.references :article
44
+ t.text :body
45
+ t.string :user
46
+ t.timestamps
47
+ end
48
+ end
49
+
50
+ class Article < ActiveRecord::Base
51
+ has_many :comments
52
+ end
53
+
54
+ class Comment < ActiveRecord::Base
55
+ belongs_to :article
56
+ end
57
+
58
+ class ApplicationController < ActionController::Base
59
+ end
60
+
61
+ def create_article(count = 1)
62
+ count.times {
63
+ Article.create! title: Faker::Lorem.sentence, body: Faker::Lorem.paragraphs(10)
64
+ }
65
+ end
66
+
67
+ def create_comment(article, count = 1)
68
+ count.times {
69
+ Comment.create! user: Faker::Name.name, body: Faker::Lorem.paragraphs(10), article_id: article.id
70
+ }
71
+ end
metadata ADDED
@@ -0,0 +1,171 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: scope_cache_key
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Carl Mercier
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-27 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 3.2.9
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 3.2.9
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: 2.13.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: 2.13.0
46
+ - !ruby/object:Gem::Dependency
47
+ name: activerecord
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 3.2.12
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: 3.2.12
62
+ - !ruby/object:Gem::Dependency
63
+ name: actionpack
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 3.2.12
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: 3.2.12
78
+ - !ruby/object:Gem::Dependency
79
+ name: pg
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: faker
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: 1.1.2
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: 1.1.2
110
+ - !ruby/object:Gem::Dependency
111
+ name: pry
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description:
127
+ email:
128
+ - carl@carlmercier.com
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - .gitignore
134
+ - .rspec
135
+ - .travis.yml
136
+ - Gemfile
137
+ - LICENSE.txt
138
+ - README.md
139
+ - Rakefile
140
+ - lib/scope_cache_key.rb
141
+ - lib/scope_cache_key/version.rb
142
+ - scope_cache_key.gemspec
143
+ - spec/scope_cache_key_spec.rb
144
+ - spec/spec_helper.rb
145
+ homepage: http://github.com/cmer/scope_cache_key
146
+ licenses: []
147
+ post_install_message:
148
+ rdoc_options: []
149
+ require_paths:
150
+ - lib
151
+ required_ruby_version: !ruby/object:Gem::Requirement
152
+ none: false
153
+ requirements:
154
+ - - ! '>='
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
157
+ required_rubygems_version: !ruby/object:Gem::Requirement
158
+ none: false
159
+ requirements:
160
+ - - ! '>='
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ requirements: []
164
+ rubyforge_project:
165
+ rubygems_version: 1.8.23
166
+ signing_key:
167
+ specification_version: 3
168
+ summary: Add cache_key functionality to scopes
169
+ test_files:
170
+ - spec/scope_cache_key_spec.rb
171
+ - spec/spec_helper.rb