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 CHANGED
@@ -1,7 +1,6 @@
1
1
  language: ruby
2
2
  rvm:
3
3
  - 1.9.3
4
- - jruby-19mode
5
4
  env: DB=sqlite
6
5
  service:
7
6
  - memcached
data/Gemfile CHANGED
@@ -13,3 +13,5 @@ platforms :jruby do
13
13
  gem "activerecord-jdbcsqlite3-adapter"
14
14
  gem "jruby-memcached"
15
15
  end
16
+
17
+ gem "debugger"
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 # post.find_cached(1)
47
- with_association :user, :comments # post.cached_user, post.cached_comments
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}
@@ -1,3 +1,3 @@
1
1
  module Cacheable
2
- VERSION = "1.3.1"
2
+ VERSION = "1.3.2"
3
3
  end
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, :cached_indices, :cached_methods
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
- association.klass.class_eval <<-EOF
98
- after_commit :expire_#{association_name}_cache
126
+ if reverse_association
127
+ association.klass.class_eval <<-EOF
128
+ after_commit :expire_#{association_name}_cache
99
129
 
100
- def expire_#{association_name}_cache
101
- if respond_to? :cached_#{reverse_association.name}
102
- # cached_viewable.expire_association_cache
103
- cached_#{reverse_association.name}.expire_association_cache(:#{association_name})
104
- else
105
- #{reverse_association.name}.#{reverse_through_association.name}.expire_association_cache(:#{association_name})
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
- end
108
- EOF
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
@@ -5,13 +5,16 @@ describe Cacheable do
5
5
 
6
6
  before :all do
7
7
  @user = User.create(:login => 'flyerhzm')
8
- @account = @user.create_account
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
@@ -1,3 +1,5 @@
1
1
  class Account < ActiveRecord::Base
2
2
  belongs_to :user
3
+
4
+ belongs_to :group
3
5
  end
@@ -0,0 +1,4 @@
1
+ class Group < ActiveRecord::Base
2
+
3
+ has_many :accounts
4
+ end
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
@@ -0,0 +1,5 @@
1
+ class Tag < ActiveRecord::Base
2
+
3
+ has_and_belongs_to_many :posts
4
+
5
+ end
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 'mocha_standalone'
7
+ require 'mocha/api'
8
8
  require 'memcached'
9
-
10
9
  require 'cacheable'
10
+ require 'debugger'
11
11
 
12
- ActiveRecord::Migration.verbose = false
13
- ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
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
- MODELS = File.join(File.dirname(__FILE__), "models")
16
- $LOAD_PATH.unshift(MODELS)
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
- Dir[ File.join(MODELS, "*.rb") ].sort.each { |file| require File.basename(file) }
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.1
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-06 00:00:00.000000000 Z
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