simple_cacheable 1.3.1 → 1.3.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +0 -1
- data/Gemfile +2 -0
- data/README.md +15 -2
- data/cacheable.gemspec +2 -2
- data/lib/cacheable/version.rb +1 -1
- data/lib/cacheable.rb +84 -14
- data/spec/cacheable_spec.rb +94 -1
- data/spec/models/account.rb +2 -0
- data/spec/models/group.rb +4 -0
- data/spec/models/post.rb +9 -2
- data/spec/models/tag.rb +5 -0
- data/spec/models/user.rb +3 -1
- data/spec/spec_helper.rb +33 -7
- metadata +8 -8
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -43,8 +43,13 @@ Usage
|
|
43
43
|
has_many :comments, :as => :commentable
|
44
44
|
|
45
45
|
model_cache do
|
46
|
-
with_key
|
47
|
-
|
46
|
+
with_key # post.find_cached(1)
|
47
|
+
with_class_method :default_post # Post.default_post
|
48
|
+
with_association :user, :comments # post.cached_user, post.cached_comments
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.default_post
|
52
|
+
Post.first
|
48
53
|
end
|
49
54
|
end
|
50
55
|
|
@@ -66,7 +71,15 @@ add the following code to your Gemfile
|
|
66
71
|
gem "simple_cacheable", :require => "cacheable"
|
67
72
|
|
68
73
|
|
74
|
+
Gotchas
|
75
|
+
-------
|
76
|
+
|
77
|
+
Caching, and caching invalidation specifically, can be hard and confusing. Simple Cacheable methods should
|
78
|
+
expire correctly in most cases. Be careful using `with_method` and `with_class_method`, they should
|
79
|
+
specifically not be used to return collections. This is demonstrated well Tobias Lutke's presentation: [Rockstar Memcaching][2].
|
80
|
+
|
69
81
|
Copyright © 2011 Richard Huang (flyerhzm@gmail.com), released under the MIT license
|
70
82
|
|
71
83
|
|
72
84
|
[1]:https://github.com/flyerhzm/rails-bestpractices.com
|
85
|
+
[2]:http://www.infoq.com/presentations/lutke-rockstar-memcaching
|
data/cacheable.gemspec
CHANGED
@@ -6,8 +6,8 @@ Gem::Specification.new do |s|
|
|
6
6
|
s.name = "simple_cacheable"
|
7
7
|
s.version = Cacheable::VERSION
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
|
-
s.authors = ["Richard Huang"]
|
10
|
-
s.email = ["flyerhzm@gmail.com"]
|
9
|
+
s.authors = ["Richard Huang", "Scott Carleton"]
|
10
|
+
s.email = ["flyerhzm@gmail.com", "scott@artsicle.com"]
|
11
11
|
s.homepage = "https://github.com/flyerhzm/simple-cacheable"
|
12
12
|
s.summary = %q{a simple cache implementation based on activerecord}
|
13
13
|
s.description = %q{a simple cache implementation based on activerecord}
|
data/lib/cacheable/version.rb
CHANGED
data/lib/cacheable.rb
CHANGED
@@ -3,9 +3,13 @@ require 'uri'
|
|
3
3
|
module Cacheable
|
4
4
|
def self.included(base)
|
5
5
|
base.class_eval do
|
6
|
-
class <<self
|
6
|
+
class << self
|
7
7
|
def model_cache(&block)
|
8
|
-
class_attribute :cached_key,
|
8
|
+
class_attribute :cached_key,
|
9
|
+
:cached_indices,
|
10
|
+
:cached_methods,
|
11
|
+
:cached_class_methods,
|
12
|
+
:cached_associations
|
9
13
|
instance_exec &block
|
10
14
|
end
|
11
15
|
|
@@ -70,9 +74,33 @@ module Cacheable
|
|
70
74
|
end
|
71
75
|
end
|
72
76
|
|
77
|
+
# Cached class method
|
78
|
+
# Should expire on any instance save
|
79
|
+
def with_class_method(*methods)
|
80
|
+
self.cached_class_methods = methods
|
81
|
+
|
82
|
+
class_eval <<-EOF
|
83
|
+
after_commit :expire_class_method_cache, on: :update
|
84
|
+
EOF
|
85
|
+
|
86
|
+
methods.each do |meth|
|
87
|
+
class_eval <<-EOF
|
88
|
+
def self.cached_#{meth}
|
89
|
+
Rails.cache.fetch class_method_cache_key("#{meth}") do
|
90
|
+
#{meth}
|
91
|
+
end
|
92
|
+
end
|
93
|
+
EOF
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
73
98
|
def with_association(*association_names)
|
99
|
+
self.cached_associations = association_names
|
100
|
+
|
74
101
|
association_names.each do |association_name|
|
75
102
|
association = reflect_on_association(association_name)
|
103
|
+
|
76
104
|
if :belongs_to == association.macro
|
77
105
|
polymorphic = association.options[:polymorphic]
|
78
106
|
polymorphic ||= false
|
@@ -90,32 +118,55 @@ module Cacheable
|
|
90
118
|
# FIXME it should be the only reflection but I'm not 100% positive
|
91
119
|
reverse_through_association = through_association.klass.reflect_on_all_associations(:belongs_to).first
|
92
120
|
|
121
|
+
# In a through association it doesn't have to be a belongs_to
|
93
122
|
reverse_association = association.klass.reflect_on_all_associations(:belongs_to).find { |reverse_association|
|
94
123
|
reverse_association.options[:polymorphic] ? reverse_association.name == association.source_reflection.options[:as] : reverse_association.klass == self
|
95
124
|
}
|
96
125
|
|
97
|
-
|
98
|
-
|
126
|
+
if reverse_association
|
127
|
+
association.klass.class_eval <<-EOF
|
128
|
+
after_commit :expire_#{association_name}_cache
|
99
129
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
130
|
+
def expire_#{association_name}_cache
|
131
|
+
if respond_to? :cached_#{reverse_association.name}
|
132
|
+
# cached_viewable.expire_association_cache
|
133
|
+
cached_#{reverse_association.name}.expire_association_cache(:#{association_name})
|
134
|
+
else
|
135
|
+
#{reverse_association.name}.#{reverse_through_association.name}.expire_association_cache(:#{association_name})
|
136
|
+
end
|
106
137
|
end
|
107
|
-
|
108
|
-
|
138
|
+
EOF
|
139
|
+
end
|
140
|
+
elsif :has_and_belongs_to_many == association.macro
|
141
|
+
# No such thing as a polymorphic has_and_belongs_to_many
|
142
|
+
reverse_association = association.klass.reflect_on_all_associations(:has_and_belongs_to_many).find { |reverse_association|
|
143
|
+
reverse_association.klass == self
|
144
|
+
}
|
145
|
+
|
146
|
+
association.klass.class_eval <<-EOF
|
147
|
+
after_commit :expire_#{association_name}_cache
|
148
|
+
|
149
|
+
def expire_#{association_name}_cache
|
150
|
+
if respond_to? :cached_#{reverse_association.name}
|
151
|
+
# cached_viewable.expire_association_cache
|
152
|
+
cached_#{reverse_association.name}.expire_association_cache(:#{association_name})
|
153
|
+
else
|
154
|
+
#{reverse_association.name}.each do |assoc|
|
155
|
+
assoc.expire_association_cache(:#{association_name})
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
EOF
|
109
160
|
else
|
110
161
|
reverse_association = association.klass.reflect_on_all_associations(:belongs_to).find { |reverse_association|
|
111
162
|
reverse_association.options[:polymorphic] ? reverse_association.name == association.options[:as] : reverse_association.klass == self
|
112
163
|
}
|
164
|
+
|
113
165
|
association.klass.class_eval <<-EOF
|
114
166
|
after_commit :expire_#{association_name}_cache
|
115
167
|
|
116
168
|
def expire_#{association_name}_cache
|
117
169
|
if respond_to? :cached_#{reverse_association.name}
|
118
|
-
# cached_viewable.expire_association_cache
|
119
170
|
cached_#{reverse_association.name}.expire_association_cache(:#{association_name})
|
120
171
|
else
|
121
172
|
#{reverse_association.name}.expire_association_cache(:#{association_name})
|
@@ -123,7 +174,6 @@ module Cacheable
|
|
123
174
|
end
|
124
175
|
EOF
|
125
176
|
end
|
126
|
-
|
127
177
|
class_eval <<-EOF
|
128
178
|
def cached_#{association_name}
|
129
179
|
Rails.cache.fetch have_association_cache_key("#{association_name}") do
|
@@ -142,8 +192,15 @@ module Cacheable
|
|
142
192
|
def all_attribute_cache_key(attribute, value)
|
143
193
|
"#{name.tableize}/attribute/#{attribute}/all/#{URI.escape(value.to_s)}"
|
144
194
|
end
|
195
|
+
|
196
|
+
def class_method_cache_key(meth)
|
197
|
+
"#{name.tableize}/class_method/#{meth}"
|
198
|
+
end
|
199
|
+
|
145
200
|
end
|
201
|
+
|
146
202
|
end
|
203
|
+
|
147
204
|
end
|
148
205
|
|
149
206
|
def expire_model_cache
|
@@ -151,6 +208,13 @@ module Cacheable
|
|
151
208
|
expire_attribute_cache if self.class.cached_indices.present?
|
152
209
|
expire_all_attribute_cache if self.class.cached_indices.present?
|
153
210
|
expire_method_cache if self.class.cached_methods.present?
|
211
|
+
expire_class_method_cache if self.class.cached_class_methods.present?
|
212
|
+
|
213
|
+
if self.class.cached_associations.present?
|
214
|
+
self.class.cached_associations.each do |assoc|
|
215
|
+
expire_association_cache(assoc)
|
216
|
+
end
|
217
|
+
end
|
154
218
|
end
|
155
219
|
|
156
220
|
def expire_key_cache
|
@@ -177,6 +241,12 @@ module Cacheable
|
|
177
241
|
end
|
178
242
|
end
|
179
243
|
|
244
|
+
def expire_class_method_cache
|
245
|
+
self.class.cached_class_methods.each do |meth|
|
246
|
+
Rails.cache.delete self.class.class_method_cache_key(meth)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
180
250
|
def expire_association_cache(name)
|
181
251
|
Rails.cache.delete have_association_cache_key(name)
|
182
252
|
end
|
data/spec/cacheable_spec.rb
CHANGED
@@ -5,13 +5,16 @@ describe Cacheable do
|
|
5
5
|
|
6
6
|
before :all do
|
7
7
|
@user = User.create(:login => 'flyerhzm')
|
8
|
-
@
|
8
|
+
@group1 = Group.create(name: "Ruby On Rails")
|
9
|
+
@account = @user.create_account(group: @group1)
|
9
10
|
@post1 = @user.posts.create(:title => 'post1')
|
10
11
|
@post2 = @user.posts.create(:title => 'post2')
|
11
12
|
@image1 = @post1.images.create
|
12
13
|
@image2 = @post1.images.create
|
13
14
|
@comment1 = @post1.comments.create
|
14
15
|
@comment2 = @post1.comments.create
|
16
|
+
@tag1 = @post1.tags.create(title: "Rails")
|
17
|
+
@tag2 = @post1.tags.create(title: "Caching")
|
15
18
|
end
|
16
19
|
|
17
20
|
before :each do
|
@@ -94,6 +97,22 @@ describe Cacheable do
|
|
94
97
|
end
|
95
98
|
end
|
96
99
|
|
100
|
+
context "with_class_method" do
|
101
|
+
it "should not cache Post.default_post" do
|
102
|
+
Rails.cache.read("posts/class_method/default_post").should be_nil
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should cache Post.default_post" do
|
106
|
+
Post.cached_default_post.should == @post1
|
107
|
+
Rails.cache.read("posts/class_method/default_post").should == @post1
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should cache Post.default_post multiple times" do
|
111
|
+
Post.cached_default_post
|
112
|
+
Post.cached_default_post.should == @post1
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
97
116
|
context "with_association" do
|
98
117
|
context "belongs_to" do
|
99
118
|
it "should not cache association" do
|
@@ -190,6 +209,49 @@ describe Cacheable do
|
|
190
209
|
end
|
191
210
|
end
|
192
211
|
|
212
|
+
context "has_one through belongs_to" do
|
213
|
+
it "should not cache associations" do
|
214
|
+
Rails.cache.read("users/#{@user.id}/association/group").should be_nil
|
215
|
+
end
|
216
|
+
|
217
|
+
it "should cache User#group" do
|
218
|
+
@user.cached_group.should == @group1
|
219
|
+
Rails.cache.read("users/#{@user.id}/association/group").should == @group1
|
220
|
+
end
|
221
|
+
|
222
|
+
it "should cache User#group multiple times" do
|
223
|
+
@user.cached_group
|
224
|
+
@user.cached_group.should == @group1
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
|
229
|
+
context "has_and_belongs_to_many" do
|
230
|
+
|
231
|
+
it "should not cache associations off the bat" do
|
232
|
+
Rails.cache.read("posts/#{@post1.id}/association/tags").should be_nil
|
233
|
+
end
|
234
|
+
|
235
|
+
it "should cache Post#tags" do
|
236
|
+
@post1.cached_tags.should == [@tag1, @tag2]
|
237
|
+
Rails.cache.read("posts/#{@post1.id}/association/tags").should == [@tag1, @tag2]
|
238
|
+
end
|
239
|
+
|
240
|
+
it "should handle multiple requests" do
|
241
|
+
@post1.cached_tags
|
242
|
+
@post1.cached_tags.should == [@tag1, @tag2]
|
243
|
+
end
|
244
|
+
|
245
|
+
context "expiry" do
|
246
|
+
it "should have the correct collection" do
|
247
|
+
@tag3 = @post1.tags.create!(title: "Invalidation is hard")
|
248
|
+
Rails.cache.read("posts/#{@post1.id}/association/tags").should be_nil
|
249
|
+
@post1.cached_tags.should == [@tag1, @tag2, @tag3]
|
250
|
+
Rails.cache.read("posts/#{@post1.id}/association/tags").should == [@tag1, @tag2, @tag3]
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
193
255
|
end
|
194
256
|
|
195
257
|
context "expire_model_cache" do
|
@@ -214,6 +276,23 @@ describe Cacheable do
|
|
214
276
|
Rails.cache.read("users/#{@user.id}/method/last_post").should be_nil
|
215
277
|
end
|
216
278
|
|
279
|
+
it "should delete with_class_method cache" do
|
280
|
+
Post.cached_default_post
|
281
|
+
Rails.cache.read("posts/class_method/default_post").should_not be_nil
|
282
|
+
@post1.expire_model_cache
|
283
|
+
Rails.cache.read("posts/class_method/default_post").should be_nil
|
284
|
+
end
|
285
|
+
|
286
|
+
it "should delete associations cache" do
|
287
|
+
@user.cached_images
|
288
|
+
Rails.cache.read("users/#{@user.id}/association/images").should_not be_nil
|
289
|
+
@user.expire_model_cache
|
290
|
+
Rails.cache.read("users/#{@user.id}/association/images").should be_nil
|
291
|
+
end
|
292
|
+
|
293
|
+
end
|
294
|
+
|
295
|
+
context "object#save" do
|
217
296
|
it "should delete has_many with_association cache" do
|
218
297
|
@user.cached_posts
|
219
298
|
Rails.cache.read("users/#{@user.id}/association/posts").should_not be_nil
|
@@ -241,5 +320,19 @@ describe Cacheable do
|
|
241
320
|
@account.save
|
242
321
|
Rails.cache.read("users/#{@user.id}/association/account").should be_nil
|
243
322
|
end
|
323
|
+
|
324
|
+
it "should delete has_and_belongs_to_many with_association cache" do
|
325
|
+
@post1.cached_tags
|
326
|
+
Rails.cache.read("posts/#{@post1.id}/association/tags").should_not be_nil
|
327
|
+
@tag1.save
|
328
|
+
Rails.cache.read("posts/#{@post1.id}/association/tags").should be_nil
|
329
|
+
end
|
330
|
+
|
331
|
+
it "should delete has_one through belongs_to with_association cache" do
|
332
|
+
@group1.save
|
333
|
+
Rails.cache.read("users/#{@user.id}/association/group").should be_nil
|
334
|
+
@user.cached_group.should == @group1
|
335
|
+
Rails.cache.read("users/#{@user.id}/association/group").should == @group1
|
336
|
+
end
|
244
337
|
end
|
245
338
|
end
|
data/spec/models/account.rb
CHANGED
data/spec/models/post.rb
CHANGED
@@ -2,13 +2,20 @@ class Post < ActiveRecord::Base
|
|
2
2
|
include Cacheable
|
3
3
|
|
4
4
|
belongs_to :user
|
5
|
-
has_many :comments, :as => :commentable
|
6
5
|
|
6
|
+
has_many :comments, :as => :commentable
|
7
7
|
has_many :images, :as => :viewable
|
8
8
|
|
9
|
+
has_and_belongs_to_many :tags
|
10
|
+
|
9
11
|
model_cache do
|
10
12
|
with_key
|
11
13
|
with_attribute :user_id
|
12
|
-
with_association :user, :comments, :images
|
14
|
+
with_association :user, :comments, :images, :tags
|
15
|
+
with_class_method :default_post
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.default_post
|
19
|
+
Post.first
|
13
20
|
end
|
14
21
|
end
|
data/spec/models/tag.rb
ADDED
data/spec/models/user.rb
CHANGED
@@ -5,11 +5,13 @@ class User < ActiveRecord::Base
|
|
5
5
|
has_one :account
|
6
6
|
has_many :images, through: :posts
|
7
7
|
|
8
|
+
has_one :group, through: :account
|
9
|
+
|
8
10
|
model_cache do
|
9
11
|
with_key
|
10
12
|
with_attribute :login
|
11
13
|
with_method :last_post
|
12
|
-
with_association :posts, :account, :images
|
14
|
+
with_association :posts, :account, :images, :group
|
13
15
|
end
|
14
16
|
|
15
17
|
def last_post
|
data/spec/spec_helper.rb
CHANGED
@@ -4,18 +4,29 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
|
|
4
4
|
require 'rails'
|
5
5
|
require 'active_record'
|
6
6
|
require 'rspec'
|
7
|
-
require '
|
7
|
+
require 'mocha/api'
|
8
8
|
require 'memcached'
|
9
|
-
|
10
9
|
require 'cacheable'
|
10
|
+
require 'debugger'
|
11
11
|
|
12
|
-
|
13
|
-
|
12
|
+
# MODELS = File.join(File.dirname(__FILE__), "models")
|
13
|
+
# $LOAD_PATH.unshift(MODELS)
|
14
|
+
# Dir[ File.join(MODELS, "*.rb") ].each { |f| require f }
|
14
15
|
|
15
|
-
|
16
|
-
|
16
|
+
# It needs this order otherwise cacheable throws
|
17
|
+
# errors when looking for reflection classes
|
18
|
+
# Specifically, post can't be before tag
|
19
|
+
# and user can't be before post
|
20
|
+
require 'models/account'
|
21
|
+
require 'models/group'
|
22
|
+
require 'models/comment'
|
23
|
+
require 'models/image'
|
24
|
+
require 'models/tag'
|
25
|
+
require 'models/post'
|
26
|
+
require 'models/user'
|
17
27
|
|
18
|
-
|
28
|
+
ActiveRecord::Migration.verbose = false
|
29
|
+
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
|
19
30
|
|
20
31
|
module Rails
|
21
32
|
class <<self
|
@@ -39,6 +50,7 @@ RSpec.configure do |config|
|
|
39
50
|
|
40
51
|
create_table :accounts do |t|
|
41
52
|
t.integer :user_id
|
53
|
+
t.integer :group_id
|
42
54
|
end
|
43
55
|
|
44
56
|
create_table :posts do |t|
|
@@ -55,7 +67,21 @@ RSpec.configure do |config|
|
|
55
67
|
t.integer :viewable_id
|
56
68
|
t.string :viewable_type
|
57
69
|
end
|
70
|
+
|
71
|
+
create_table :tags do |t|
|
72
|
+
t.string :title
|
73
|
+
end
|
74
|
+
|
75
|
+
create_table :posts_tags, id: false do |t|
|
76
|
+
t.integer :post_id
|
77
|
+
t.integer :tag_id
|
78
|
+
end
|
79
|
+
|
80
|
+
create_table :groups do |t|
|
81
|
+
t.string :name
|
82
|
+
end
|
58
83
|
end
|
84
|
+
|
59
85
|
end
|
60
86
|
|
61
87
|
config.after :all do
|
metadata
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simple_cacheable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.3.
|
4
|
+
version: 1.3.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Richard Huang
|
9
|
+
- Scott Carleton
|
9
10
|
autorequire:
|
10
11
|
bindir: bin
|
11
12
|
cert_chain: []
|
12
|
-
date: 2012-12-
|
13
|
+
date: 2012-12-31 00:00:00.000000000 Z
|
13
14
|
dependencies:
|
14
15
|
- !ruby/object:Gem::Dependency
|
15
16
|
name: rails
|
@@ -62,6 +63,7 @@ dependencies:
|
|
62
63
|
description: a simple cache implementation based on activerecord
|
63
64
|
email:
|
64
65
|
- flyerhzm@gmail.com
|
66
|
+
- scott@artsicle.com
|
65
67
|
executables: []
|
66
68
|
extensions: []
|
67
69
|
extra_rdoc_files: []
|
@@ -79,8 +81,10 @@ files:
|
|
79
81
|
- spec/cacheable_spec.rb
|
80
82
|
- spec/models/account.rb
|
81
83
|
- spec/models/comment.rb
|
84
|
+
- spec/models/group.rb
|
82
85
|
- spec/models/image.rb
|
83
86
|
- spec/models/post.rb
|
87
|
+
- spec/models/tag.rb
|
84
88
|
- spec/models/user.rb
|
85
89
|
- spec/spec_helper.rb
|
86
90
|
homepage: https://github.com/flyerhzm/simple-cacheable
|
@@ -95,18 +99,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
95
99
|
- - ! '>='
|
96
100
|
- !ruby/object:Gem::Version
|
97
101
|
version: '0'
|
98
|
-
segments:
|
99
|
-
- 0
|
100
|
-
hash: -672054861354724444
|
101
102
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
103
|
none: false
|
103
104
|
requirements:
|
104
105
|
- - ! '>='
|
105
106
|
- !ruby/object:Gem::Version
|
106
107
|
version: '0'
|
107
|
-
segments:
|
108
|
-
- 0
|
109
|
-
hash: -672054861354724444
|
110
108
|
requirements: []
|
111
109
|
rubyforge_project:
|
112
110
|
rubygems_version: 1.8.24
|
@@ -117,7 +115,9 @@ test_files:
|
|
117
115
|
- spec/cacheable_spec.rb
|
118
116
|
- spec/models/account.rb
|
119
117
|
- spec/models/comment.rb
|
118
|
+
- spec/models/group.rb
|
120
119
|
- spec/models/image.rb
|
121
120
|
- spec/models/post.rb
|
121
|
+
- spec/models/tag.rb
|
122
122
|
- spec/models/user.rb
|
123
123
|
- spec/spec_helper.rb
|