arid_cache 1.3.0 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -8,6 +8,7 @@ AridCache simplifies caching by supporting auto-expiring cache keys - as well as
8
8
 
9
9
  == Changes
10
10
 
11
+ v1.3.1: Support proxies which allow you to control how your objects get serialized and unserialized
11
12
  v1.3.0: Support limits, ordering and pagination on cached Enumerables
12
13
  v1.2.0: Fix Rails 3 ActiveRecord hooks & remove some Rails dependencies
13
14
  v1.0.5: Support <tt>:raw</tt> and <tt>:clear</tt> options.
@@ -42,11 +43,12 @@ Then
42
43
  * Supports limits, ordering & pagination of cached Enumerables and ActiveRecord collections
43
44
  * Define caches and their options on your class using +instance_caches+ and +class_caches+
44
45
  * Counts for free - if you have already cached the result, you get the count for free
45
- * Supports eager-loading and other options to <tt>ActiveRecord::Base#find</tt> like
46
- :conditions, :include, :joins, :select, :readonly, :group, :having, :from
46
+ * Smart counts - if you only ask for the count it will only calculate the count; useful when the result is an Association Reflection or a named scope.
47
+ * Supports eager-loading and other options to <tt>ActiveRecord::Base#find</tt> like <tt>:conditions</tt>, <tt>:include</tt>, <tt>:joins</tt>, <tt>:select</tt>, <tt>:readonly</tt>, <tt>:group</tt>, <tt>:having</tt>, <tt>:from</tt>
47
48
  * Provides methods to clear caches individually, at the instance-level, class-level and globally
48
49
  * Preserves the order of your cached ActiveRecord collections
49
50
  * Optimized to make as few cache and database accesses as absolutely neccessary
51
+ * Define your own cache proxy to serialize your objects as they go to and from the cache
50
52
 
51
53
  == Introduction
52
54
 
@@ -63,10 +65,25 @@ The way you interact with the cache via your model methods is to prepend the met
63
65
 
64
66
  You can also define caches that use compositions of methods or named scopes, or other complex queries, without having to add a new method to your class. This way you can also create different caches that all use the same method. For example,
65
67
 
66
- User.cached_most_active_users do
67
- active.find(:order => 'activity DESC', :limit => 5)
68
+ class User
69
+ named_scope :active, :conditions => { :active => true }
70
+ class_caches do
71
+ most_active_users(:limit => 5) do
72
+ active.find(:order => 'activity DESC')
73
+ end
74
+ end
68
75
  end
69
76
 
77
+ This defines a cache +most_active_users+ on the User class which we can call with:
78
+
79
+ User.cached_most_active_users
80
+ >> [#<User>, #<User>]
81
+
82
+ This will return up to five users, but there may be many more in the cache, because we didn't apply a limit in our call to <tt>active.find()</tt>. We can also pass options to the call to override the stored options:
83
+
84
+ User.cached_most_active_users(:limit => 1)
85
+ >> [#<User>]
86
+
70
87
  === ActiveRecord Collections
71
88
 
72
89
  If the result of your <tt>cached_</tt> call is an array of ActiveRecords, AridCache only stores the IDs in the cache (because it's a bad idea to store records in the cache).
@@ -358,6 +375,65 @@ In some circumstances - like when you are querying on a named scope - if you hav
358
375
  :ids => [2, 235, 236, 237] # the ids array is seeded before returning
359
376
  }
360
377
 
378
+ == Proxies
379
+
380
+ Proxies allow you to do anything you want to your objects as they go into - and come out of - the cache. They are most useful for serializing your objects, for example if you want to store JSON, or hashes.
381
+
382
+ The <tt>:proxy</tt> option should be a Symbol giving the name of a class method. The method is passed the result of your cache block (or method) as the first parameter, and in future may also pass a Hash of options. The method is called on the class that calls +cached_+.
383
+
384
+ A simple example of a proxy which stores hashes of ActiveRecord attributes:
385
+
386
+ class User
387
+ has_many :companies
388
+
389
+ instance_caches do
390
+ companies(:proxy => :serializing_proxy)
391
+ end
392
+
393
+ def self.serializing_proxy(records, *args)
394
+ return records if records.empty?
395
+ records.first.is_a?(ActiveRecord::Base) ? records.collect(&:attributes) : records.collect { |r| Company.find_by_id(r['id']) }
396
+ end
397
+ end
398
+
399
+ Now when we first call <tt>User.first.cached_companies<tt> the cache is empty, so the cache method (<tt>companies</tt>) is called and the result is passed to <tt>User.serializing_proxy</tt> which converts the records to Hashes. The result of the proxy is stored in the cache. The next time we call <tt>User.first.cached_companies<tt> the cached result is retrieved and passed to <tt>User.serializing_proxy</tt> which converts the hashes to records, so we get ActiveRecords back.
400
+
401
+ @user = User.first
402
+ @user.cached_companies
403
+ >> [#<Company id: 1>, #<Company id: 2>]
404
+
405
+ @user.cached_companies(:raw => true)
406
+ >> [{ 'id' => 1 }, { 'id' => 2 }]
407
+
408
+ The <tt>:raw</tt> option bypasses the proxy on the way out of the cache because we are asking for a raw result. The <tt>:raw</tt> option works a bit differently from the default behaviour (which is to return a <tt>CachedResult</tt> object if the cached result is an ActiveRecord collection) because you can use options to do limiting, pagination and in-memory ordering with <tt>:raw</tt> to get different views of your cached enumerable.
409
+
410
+ Proxies still support limits, pagination and ordering, which is applied to the cached result. For example:
411
+
412
+ Limits:
413
+
414
+ @user.cached_companies(:limit => 2, :offset => 1)
415
+ >> [#<Company id: 2>]
416
+
417
+ Pagination returns a <tt>WillPaginate::Collection</tt>:
418
+
419
+ result = @user.cached_companies(:page => 2, :per_page => 1)
420
+ >> [#<Company id: 2>]
421
+ result.total_entries
422
+ >> 1
423
+ result.current_page
424
+ >> 2
425
+
426
+ Order by:
427
+
428
+ @user.cached_companies(:order => Proc.new { |a, b| b['id'] <=> a['id'] })
429
+ >> [#<Company id: 2>, #<Company id: 1>]
430
+
431
+ The above Proc orders by id reversed. We can also combine all the options. The order by will be applied first, then the limits and finally the pagination.
432
+
433
+ All of the above calls work with <tt>raw => true</tt>. With that option the results would be same but instead of returning records, it returns hashes.
434
+
435
+ In practice you probably want to define your proxy method on <tt>ActiveRecord::Base</tt> so that it is available to all your models. In future AridCache will probably have some built-in proxies that you can make use of.
436
+
361
437
  == Efficiency
362
438
 
363
439
  * AridCache intercepts calls to <tt>cached_</tt> methods using <tt>method_missing</tt> then defines those methods on your models as they are called, so they bypass method missing on subsequent calls.
@@ -383,7 +459,7 @@ For Ruby < 1.8.7 you probably want to include the following to extend the Array
383
459
  == Known Issues
384
460
 
385
461
  1. <b>Caches that contains duplicate records will only return unique records on subsequent calls</b>. This is because of the way <tt>find</tt> works when selecting multiple ids. For example, if your query returns <tt>[#<User id: 1>, #<User id: 1>, #<User id: 1>]</tt>, the IDs are cached as <tt>[1,1,1]</tt>. On the next call to the cache we load the IDs using <tt>User.find_all_by_id([1,1,1])</tt> which returns <tt>[#<User id: 1>]</tt>, not <tt>[#<User id: 1>, #<User id: 1>, #<User id: 1>]</tt> as you might have expected.
386
- 2. <b>You can't cache polymorphic arrays</b> e.g. [#<User id: 1>, #<Pet id: 5>] because it expects all ActiveRecords to be of the same class. We could accept a <tt>:polymorphic => true</tt> option but I don't think this is a great idea because instantiating all the records would result in a lot of queries to the individual tables.
462
+ 2. <b>You can't cache polymorphic arrays</b> e.g. [#<User id: 1>, #<Pet id: 5>] because it expects all ActiveRecords to be of the same class. If you need polymorphism consider using a proxy.
387
463
 
388
464
  == Contributors
389
465
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.3.0
1
+ 1.3.1
data/arid_cache.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{arid_cache}
8
- s.version = "1.3.0"
8
+ s.version = "1.3.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Karl Varga"]
12
- s.date = %q{2011-04-05}
12
+ s.date = %q{2011-04-06}
13
13
  s.description = %q{AridCache makes caching easy and effective. AridCache supports caching on all your model named scopes, class methods and instance methods right out of the box. AridCache prevents caching logic from cluttering your models and clarifies your logic by making explicit calls to cached result sets.
14
14
  AridCache is designed for handling large, expensive ActiveRecord collections but is equally useful for caching anything else as well.
15
15
  }
@@ -36,7 +36,9 @@ module AridCache
36
36
  elsif options[:auto_expire]
37
37
  self.cache_key
38
38
  else
39
- "#{AridCache::Inflector.pluralize(self.class.name.downcase)}/#{self.id}"
39
+ result = "#{AridCache::Inflector.pluralize(self.class.name.downcase)}"
40
+ result += "/#{self[:id]}" if self.respond_to?(:[]) && !self[:id].nil?
41
+ result
40
42
  end
41
43
  'arid-cache-' + object_key + '-' + key.to_s
42
44
  end
@@ -67,6 +67,10 @@ module AridCache
67
67
 
68
68
  def order_by_key?
69
69
  include?(:order) && (self[:order].is_a?(Symbol) || self[:order].is_a?(String))
70
+ end
71
+
72
+ def proxy?
73
+ include?(:proxy)
70
74
  end
71
75
  end
72
76
  end
@@ -6,6 +6,11 @@ module AridCache
6
6
  # Provides methods to introspect the result. The contents could be a base type,
7
7
  # or an enumerable of sorts...any type really. We are only concerned with enumerables,
8
8
  # and especially those containing active records.
9
+ #
10
+ # TODO: a lot of this logic should be encompassed in the CachedResult. It's probably
11
+ # a good idea to always cache a CachedResult and move the result-related methods
12
+ # into that class. We have to keep whatever is cached as small as possible tho,
13
+ # so it's probably best to cache a Hash and load it with CachedResult.
9
14
  class ResultProcessor
10
15
 
11
16
  def initialize(result, opts={})
@@ -54,23 +59,29 @@ module AridCache
54
59
  # Check if it's an association first, because it doesn't trigger the select if it's
55
60
  # a named scope. Calling respond_to? on an association proxy will trigger a select
56
61
  # because it loads up the target and passes the respond_to? on to it.
57
- @cached = if is_activerecord_reflection?
58
- lazy_cache.klass = @result.proxy_reflection.klass if @result.respond_to?(:proxy_reflection)
59
- if @options.count_only?
60
- lazy_cache.count = @result.count
61
- else
62
+ @cached =
63
+ if @options.proxy?
64
+ if is_activerecord_reflection?
65
+ @result = @result.collect { |r| r } # force it to load
66
+ end
67
+ Utilities.object_class(@options[:receiver]).send(@options[:proxy], @result)
68
+ elsif is_activerecord_reflection?
69
+ lazy_cache.klass = @result.proxy_reflection.klass if @result.respond_to?(:proxy_reflection)
70
+ if @options.count_only?
71
+ lazy_cache.count = @result.count
72
+ else
73
+ lazy_cache.ids = @result.collect { |r| r[:id] }
74
+ lazy_cache.count = @result.size
75
+ end
76
+ lazy_cache
77
+ elsif is_activerecord? || is_empty?
62
78
  lazy_cache.ids = @result.collect { |r| r[:id] }
63
79
  lazy_cache.count = @result.size
80
+ lazy_cache.klass = @result.first.class
81
+ lazy_cache
82
+ else
83
+ @result
64
84
  end
65
- lazy_cache
66
- elsif is_activerecord? || is_empty?
67
- lazy_cache.ids = @result.collect { |r| r[:id] }
68
- lazy_cache.count = @result.size
69
- lazy_cache.klass = @result.first.class
70
- lazy_cache
71
- else
72
- @result
73
- end
74
85
  end
75
86
 
76
87
  # Apply any options like pagination or ordering and return the result, which
@@ -78,16 +89,32 @@ module AridCache
78
89
  def to_result
79
90
  if @options.count_only?
80
91
  get_count
81
- elsif @options.raw? || (!is_cached_result? && !is_enumerable?)
92
+ elsif is_cached_result? && @options.raw?
82
93
  @result
83
- else
84
- if is_cached_result?
85
- fetch_activerecords(filter_results(@result.ids))
86
- elsif order_in_database?
87
- fetch_activerecords(filter_results(@result))
94
+ elsif @options.proxy?
95
+ results =
96
+ if @cached.nil? || !@options.raw?
97
+ @result
98
+ else
99
+ @cached
100
+ end
101
+ filtered = filter_results(results)
102
+ if @cached.nil? && !@options.raw?
103
+ proxy_result = Utilities.object_class(@options[:receiver]).send(@options[:proxy], filtered)
104
+ if filtered.is_a?(WillPaginate::Collection) && proxy_result.is_a?(Enumerable)
105
+ filtered.replace(proxy_result)
106
+ else
107
+ proxy_result
108
+ end
88
109
  else
89
- filter_results(@result)
110
+ filtered
90
111
  end
112
+ elsif is_cached_result?
113
+ fetch_activerecords(filter_results(@result.ids))
114
+ elsif order_in_database?
115
+ fetch_activerecords(filter_results(@result))
116
+ else
117
+ filter_results(@result)
91
118
  end
92
119
  end
93
120
 
@@ -76,5 +76,11 @@ describe AridCache::CacheProxy::Options do
76
76
  it "should use find_all_by_id as the finder" do
77
77
  new_options.opts_for_paginate[:finder].should == :find_all_by_id
78
78
  end
79
+ end
80
+
81
+ describe "proxies" do
82
+ it "should use proxy" do
83
+ new_options(:proxy => :serializing_proxy).proxy?.should be_true
84
+ end
79
85
  end
80
86
  end
@@ -179,8 +179,8 @@ describe AridCache::CacheProxy::ResultProcessor do
179
179
  it "should order hashes by symbol key" do
180
180
  new_result(@hashes, :order => :high).to_result.collect { |h| h[:high] }.should == @high
181
181
  end
182
- end
183
-
182
+ end
183
+
184
184
  describe "paginating arrays" do
185
185
  before :each do
186
186
  @value = (1..10).to_a
@@ -191,27 +191,168 @@ describe AridCache::CacheProxy::ResultProcessor do
191
191
  @result.should be_a(WillPaginate::Collection)
192
192
  @result.total_entries.should == @value.size
193
193
  @result.current_page.should == 1
194
- end
195
-
194
+ end
195
+
196
196
  it "should handle per_page option" do
197
197
  @result = new_result(@value, :page => 3, :per_page => 3).to_result
198
198
  @result.should be_a(WillPaginate::Collection)
199
199
  @result.total_entries.should == @value.size
200
200
  @result.current_page.should == 3
201
- @result.per_page.should == 3
201
+ @result.per_page.should == 3
202
202
  end
203
203
  end
204
-
205
- it "should order limit and then paginate all at once" do
204
+
205
+ it "should order limit and then paginate all at once" do
206
206
  # It will reverse it, offset 2, limit 15, then paginate
207
207
  @options = {
208
- :limit => 15,
209
- :offset => 2,
208
+ :limit => 15,
209
+ :offset => 2,
210
210
  :order => Proc.new { |a, b| b <=> a },
211
- :page => 2,
211
+ :page => 2,
212
212
  :per_page => 5
213
213
  }
214
214
  @result = new_result((1..20).to_a, @options).to_result.should == [13, 12, 11, 10, 9]
215
215
  end
216
216
  end
217
+
218
+ describe "proxies" do
219
+ before :each do
220
+ class User
221
+ instance_caches do
222
+ companies(:proxy => :abc)
223
+ end
224
+
225
+ def self.abc(records)
226
+ return records if records.empty?
227
+ records.first.is_a?(ActiveRecord::Base) ? records.collect(&:attributes) : records.collect { |r| Company.find_by_id(r['id']) }
228
+ end
229
+ end
230
+
231
+ @user = User.make
232
+ @company = Company.make
233
+ @user.companies << @company
234
+ @user.cached_companies(:clear => true)
235
+ end
236
+
237
+ it "the proxy called on itself should return the original value" do
238
+ User.abc(User.abc([@company])).should == [@company]
239
+ end
240
+
241
+ it "should serialize" do
242
+ @user.cached_companies.should == [@company]
243
+ end
244
+
245
+ it "should un-serialize" do
246
+ @user.cached_companies # seed the cache
247
+ @user.cached_companies.should == [@company]
248
+ end
249
+
250
+ it "should return json with :raw => true" do
251
+ # Seed the cache; it should use the serialized result in the return value.
252
+ value = @user.cached_companies(:raw => true)
253
+
254
+ # Comparing the hashes directly doesn't work because the updated_at Time are
255
+ # not considered equal...don't know why, cause the to_s looks the same.
256
+ # debugger
257
+ value.should be_a(Array)
258
+ value.first.should be_a(Hash)
259
+ value.first.each_pair do |k, v|
260
+ v.to_s.should == @company.attributes[k].to_s
261
+ end
262
+
263
+ # Cache is seeded, it should use the cached result
264
+ User.expects(:abc).never
265
+ value = @user.cached_companies(:raw => true)
266
+ value.should be_a(Array)
267
+ value.first.should be_a(Hash)
268
+ value.first.each_pair do |k, v|
269
+ v.to_s.should == @company.attributes[k].to_s
270
+ end
271
+ end
272
+
273
+ describe "with options" do
274
+ before :each do
275
+ @c2 = Company.make
276
+ @c3 = Company.make
277
+ @user.companies << @c2
278
+ @user.companies << @c3
279
+ end
280
+
281
+ it "should apply limit and offset" do
282
+ @companies = @user.cached_companies
283
+ @user.cached_companies(:limit => 2, :offset => 1).should == @companies[1,2]
284
+ end
285
+
286
+ it "should apply limit and offset with :raw => true" do
287
+ @companies = @user.cached_companies
288
+ @user.cached_companies(:limit => 2, :offset => 1, :raw => true).should == User.abc(@companies[1,2])
289
+ end
290
+
291
+ it "should apply pagination" do
292
+ @companies = @user.cached_companies
293
+ value = @user.cached_companies(:page => 2, :per_page => 2)
294
+ value.should be_a(WillPaginate::Collection)
295
+ value.first.should be_a(Company)
296
+ value.first.should == @companies[-1]
297
+ value.total_entries.should == @companies.size
298
+ value.current_page.should == 2
299
+ value.size.should == 1
300
+ end
301
+
302
+ it "should get the count for free" do
303
+ lambda { @user.cached_companies_count }.should query(1)
304
+ lambda { @user.cached_companies_count }.should query(0)
305
+ end
306
+
307
+ it "should paginate with :raw => true" do
308
+ @companies = User.abc(@user.cached_companies)
309
+ value = @user.cached_companies(:page => 2, :per_page => 2, :raw => true)
310
+ value.should be_a(WillPaginate::Collection)
311
+ value.first.should be_a(Hash)
312
+ value.first.should == @companies[-1]
313
+ value.total_entries.should == @companies.size
314
+ value.current_page.should == 2
315
+ value.size.should == 1
316
+ end
317
+
318
+ it "should order by" do
319
+ @companies = @user.cached_companies
320
+ @user.cached_companies(:order => 'id').should == @companies
321
+ @user.cached_companies(:order => Proc.new { |a, b| b['id'] <=> a['id'] }).should == @companies.reverse
322
+ end
323
+
324
+ it "should order by with :raw => true" do
325
+ @companies = @user.cached_companies(:raw => true)
326
+ @companies.first.should be_a(Hash)
327
+ @user.cached_companies(:order => 'id', :raw => true).should == @companies
328
+ @user.cached_companies(:order => Proc.new { |a, b| b['id'] <=> a['id'] }, :raw => true).should == @companies.reverse
329
+ end
330
+
331
+ it "should apply all options at once" do
332
+ @companies = @user.cached_companies
333
+ value = @user.cached_companies(
334
+ :order => Proc.new { |a, b| b['id'] <=> a['id'] },
335
+ :limit => 2, :offset => 1,
336
+ :page => 1, :per_page => 2)
337
+ value.should be_a(WillPaginate::Collection)
338
+ value.current_page.should == 1
339
+ value.total_entries.should == 2
340
+ value.to_a.should == [@companies[1], @companies[0]]
341
+ end
342
+
343
+ it "should apply all options with :raw => true" do
344
+ @companies = @user.cached_companies(:raw => true)
345
+ value = @user.cached_companies(
346
+ :order => Proc.new { |a, b| b['id'] <=> a['id'] },
347
+ :limit => 2, :offset => 1,
348
+ :page => 1, :per_page => 2,
349
+ :raw => true)
350
+ value.should be_a(WillPaginate::Collection)
351
+ value.current_page.should == 1
352
+ value.total_entries.should == 2
353
+ value.first.should be_a(Hash)
354
+ value.to_a.should == [@companies[1], @companies[0]]
355
+ end
356
+ end
357
+ end
217
358
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: arid_cache
3
3
  version: !ruby/object:Gem::Version
4
- hash: 27
4
+ hash: 25
5
5
  prerelease:
6
6
  segments:
7
7
  - 1
8
8
  - 3
9
- - 0
10
- version: 1.3.0
9
+ - 1
10
+ version: 1.3.1
11
11
  platform: ruby
12
12
  authors:
13
13
  - Karl Varga
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-04-05 00:00:00 -07:00
18
+ date: 2011-04-06 00:00:00 -07:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency