simple_cacheable 1.3.1 → 1.3.2

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/.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