arid_cache 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,13 +1,15 @@
1
+ require 'arid_cache/cache_proxy/utilities'
2
+ require 'arid_cache/cache_proxy/options'
3
+ require 'arid_cache/cache_proxy/result_processor'
4
+
1
5
  module AridCache
2
6
  class CacheProxy
3
- attr_accessor :object, :key, :opts, :blueprint, :cached, :cache_key, :block, :records, :combined_options, :klass
4
7
 
5
- # AridCache::CacheProxy::Result
8
+ # AridCache::CacheProxy::CachedResult
6
9
  #
7
- # This struct is stored in the cache and stores information we need
8
- # to re-query for results.
9
- Result = Struct.new(:ids, :klass, :count) do
10
-
10
+ # This struct is stored in the cache and stores information about a
11
+ # collection of ActiveRecords.
12
+ CachedResult = Struct.new(:ids, :klass, :count) do
11
13
  def has_count?
12
14
  !count.nil?
13
15
  end
@@ -25,6 +27,12 @@ module AridCache
25
27
  end
26
28
  end
27
29
 
30
+ OPTIONS_FOR_PAGINATE = [:page, :per_page, :total_entries, :finder]
31
+ OPTIONS_FOR_CACHE_PROXY = [:raw, :clear]
32
+ OPTIONS_FOR_FIND = [ :conditions, :include, :joins, :limit, :offset, :order, :select, :readonly, :group, :having, :from, :lock ]
33
+ OPTIONS_FOR_CACHE = [ :expires_in ]
34
+ OPTIONS_FOR_CACHE_KEY = [ :auto_expire ]
35
+
28
36
  #
29
37
  # Managing your caches
30
38
  #
@@ -34,26 +42,36 @@ module AridCache
34
42
  end
35
43
 
36
44
  def self.clear_class_caches(object)
37
- key = (object.is_a?(Class) ? object : object.class).name.downcase + '-'
45
+ key = (Utilities.object_class(object)).name.downcase + '-'
38
46
  Rails.cache.delete_matched(%r[arid-cache-#{key}.*])
39
47
  end
40
48
 
41
49
  def self.clear_instance_caches(object)
42
- key = AridCache::Inflector.pluralize((object.is_a?(Class) ? object : object.class).name).downcase
50
+ key = AridCache::Inflector.pluralize((Utilities.object_class(object)).name).downcase
43
51
  Rails.cache.delete_matched(%r[arid-cache-#{key}.*])
44
52
  end
45
53
 
46
- def initialize(object, key, opts={}, &block)
47
- self.object = object
48
- self.key = key
49
- self.opts = opts.symbolize_keys
50
- self.blueprint = AridCache.store.find(object, key)
51
- self.block = block
52
- self.records = nil
54
+ # Clear the cached result for this cache only
55
+ def clear_cached
56
+ Rails.cache.delete(@cache_key, @options.opts_for_cache)
57
+ end
58
+
59
+ #
60
+ # Initialize
61
+ #
53
62
 
54
- # The options from the blueprint merged with the options for this call
55
- self.combined_options = self.blueprint.nil? ? self.opts : self.blueprint.opts.merge(self.opts)
56
- self.cache_key = object.arid_cache_key(key, opts_for_cache_key)
63
+ def initialize(receiver, method, opts={}, &block)
64
+ @receiver = receiver
65
+ @method = method
66
+ @block = block
67
+ @blueprint = AridCache.store.find(@receiver, @method)
68
+
69
+ # Combine the options from the blueprint with the options for this call
70
+ opts = opts.symbolize_keys
71
+ @options = Options.new(@blueprint.nil? ? opts : @blueprint.opts.merge(opts))
72
+ @options[:receiver] = receiver
73
+ @cache_key = @receiver.arid_cache_key(@method, @options.opts_for_cache_key)
74
+ @cached = Rails.cache.read(@cache_key, @options.opts_for_cache)
57
75
  end
58
76
 
59
77
  #
@@ -61,308 +79,49 @@ module AridCache
61
79
  #
62
80
 
63
81
  # Return a count of ids in the cache, or return whatever is in the cache if it is
64
- # not a CacheProxy::Result
82
+ # not a CacheProxy::CachedResult
65
83
  def fetch_count
66
- if refresh_cache?
67
- execute_count
68
- elsif cached.is_a?(AridCache::CacheProxy::Result)
69
- cached.has_count? ? cached.count : execute_count
70
- elsif cached.is_a?(Fixnum)
71
- cached
72
- elsif cached.respond_to?(:count)
73
- cached.count
74
- else
75
- cached # what else can we do? return it
76
- end
84
+ @options[:count_only] = true
85
+ result_processor.to_result
77
86
  end
78
87
 
79
88
  # Return a list of records using the options provided. If the item in the cache
80
- # is not a CacheProxy::Result it is returned as-is. If there is nothing in the cache
89
+ # is not a CacheProxy::CachedResult it is returned after applying options. If there is nothing in the cache
81
90
  # the block defining the cache is exectued. If the :raw option is true, returns the
82
- # CacheProxy::Result unmodified, ignoring other options, except where those options
83
- # are used to initialize the cache.
91
+ # CacheProxy::CachedResult unmodified, ignoring other options, except where those options
92
+ # are needed to initialize the cache.
84
93
  def fetch
85
- @raw_result = opts_for_cache_proxy[:raw] == true
86
-
87
- result = if refresh_cache?
88
- execute_find(@raw_result)
89
- elsif cached.is_a?(AridCache::CacheProxy::Result)
90
- if cached.has_ids? && @raw_result
91
- self.cached # return it unmodified
92
- elsif cached.has_ids?
93
- fetch_from_cache # return a list of active records after applying options
94
- else # true if we have only calculated the count thus far
95
- execute_find(@raw_result)
96
- end
97
- else
98
- cached # some base type, return it unmodified
99
- end
100
- end
101
-
102
- # Clear the cached result for this cache only
103
- def clear_cached
104
- Rails.cache.delete(self.cache_key, opts_for_cache)
105
- end
106
-
107
- # Return the cached result for this object's key
108
- def cached
109
- @cached ||= Rails.cache.read(self.cache_key, opts_for_cache)
94
+ result_processor.to_result
110
95
  end
111
96
 
112
- # Return the class of the cached results i.e. if the cached result is a
113
- # list of Album records, then klass returns Album. If there is nothing
114
- # in the cache, then the class is inferred to be the class of the object
115
- # that the cached method is being called on.
116
- def klass
117
- @klass ||= if self.cached && self.cached.is_a?(AridCache::CacheProxy::Result)
118
- self.cached.klass
119
- else
120
- object_base_class
121
- end
122
- end
123
-
124
-
125
97
  private
126
98
 
127
- # Return a list of records from the database using the ids from
128
- # the cached Result.
129
- #
130
- # The result is paginated if the :page option is preset, otherwise
131
- # a regular list of ActiveRecord results is returned.
132
- #
133
- # If no :order is specified, the current ordering of the ids is
134
- # preserved with some fancy SQL.
135
- def fetch_from_cache
136
- if paginate?
137
-
138
- # Return a paginated collection
139
- if cached.ids.empty?
140
-
141
- # No ids, return an empty WillPaginate result
142
- [].paginate(opts_for_paginate)
143
-
144
- elsif combined_options.include?(:order)
145
-
146
- # An order has been specified. We have to go to the database
147
- # and paginate there because the contents of the requested
148
- # page will be different.
149
- klass.paginate(cached.ids, { :total_entries => cached.ids.size }.merge(opts_for_find.merge(opts_for_paginate)))
150
-
151
- else
152
-
153
- # Order is unchanged. We can paginate in memory and only select
154
- # those ids that we actually need. This is the most efficient.
155
- paged_ids = cached.ids.paginate(opts_for_paginate)
156
- paged_ids.replace(klass.find_all_by_id(paged_ids, opts_for_find(paged_ids)))
157
-
158
- end
159
-
160
- elsif cached.ids.empty?
161
-
162
- # We are returning a regular (non-paginated) result.
163
- # If we don't have any ids, that's an empty result
164
- # in any language.
165
- []
166
-
167
- elsif combined_options.include?(:order)
168
-
169
- # An order has been specified, so use it.
170
- klass.find_all_by_id(cached.ids, opts_for_find)
171
-
172
- else
173
-
174
- # No order has been specified, so we have to maintain
175
- # the current order. We do this by passing some extra
176
- # SQL which orders by the current array ordering.
177
- offset, limit = combined_options.delete(:offset) || 0, combined_options.delete(:limit) || cached.count
178
- ids = cached.ids[offset, limit]
179
-
180
- klass.find_all_by_id(ids, opts_for_find(ids))
181
- end
99
+ # Return a ResultProcessor instance. Seed the cache if we need to, otherwise
100
+ # use what is in the cache.
101
+ def result_processor
102
+ seed_cache? ? seed_cache : ResultProcessor.new(@cached, @options)
182
103
  end
183
104
 
184
- def paginate?
185
- combined_options.include?(:page)
105
+ # Return a boolean indicating whether we need to seed the cache. Seed the cache
106
+ # if :force => true, the cache is empty or records have been requested and there
107
+ # are none in the cache yet.
108
+ def seed_cache?
109
+ @cached.nil? || @options.force? || (@cached.is_a?(CachedResult) && !@options.count_only? && !@cached.has_ids?)
186
110
  end
187
111
 
188
- def refresh_cache?
189
- cached.nil? || opts[:force]
190
- end
191
-
192
- def get_records
193
- block = self.block || (blueprint && blueprint.proc)
194
- self.records = block.nil? ? object.instance_eval(key) : object.instance_eval(&block)
195
- end
196
-
197
- # Seed the cache by executing the stored block (or by calling a method on the object).
198
- # Then apply any options like pagination or ordering before returning the result, which
199
- # is either some base type, or usually, a list of active records.
200
- #
201
- # Options:
202
- # raw - if true, return the CacheProxy::Result after seeding the cache, ignoring
203
- # other options. Default is false.
204
- def execute_find(raw = false)
205
- get_records
206
- cached = AridCache::CacheProxy::Result.new
207
-
208
- if !records.is_a?(Enumerable) || (!records.empty? && !records.first.is_a?(::ActiveRecord::Base))
209
- cached = records # some base type, cache it as itself
210
- else
211
- cached.ids = records.collect(&:id)
212
- cached.count = records.size
213
- if records.respond_to?(:proxy_reflection) # association proxy
214
- cached.klass = records.proxy_reflection.klass
215
- elsif !records.empty?
216
- cached.klass = records.first.class
217
- else
218
- cached.klass = object_base_class
219
- end
220
- end
221
- Rails.cache.write(cache_key, cached, opts_for_cache)
222
- self.cached = cached
223
-
224
- # Return the raw result?
225
- return self.cached if raw
226
-
227
- # An order has been specified. We have to go to the database
228
- # to order because we can't be sure that the current order is the same as the cache.
229
- if cached.is_a?(AridCache::CacheProxy::Result) && combined_options.include?(:order)
230
- self.klass = self.cached.klass # TODO used by fetch_from_cache needs refactor
231
- fetch_from_cache
232
- else
233
- process_result_in_memory(records)
234
- end
112
+ # Seed the cache by executing the stored block (or by calling a method on the object)
113
+ # and storing the result in the cache. Return the processed result ready to return
114
+ # to the user.
115
+ def seed_cache
116
+ block = @block || (@blueprint && @blueprint.proc)
117
+ block_result = block.nil? ? @receiver.instance_eval(@method) : @receiver.instance_eval(&block)
118
+ @result = ResultProcessor.new(block_result, @options)
119
+ write_cache(@result.to_cache)
120
+ @result
235
121
  end
236
122
 
237
- # Convert records to an array before calling paginate. If we don't do this
238
- # and the result is a named scope, paginate will trigger an additional query
239
- # to load the page rather than just using the records we have already fetched.
240
- #
241
- # If we are not paginating and the options include :limit (and optionally :offset)
242
- # apply the limit and offset to the records before returning them.
243
- #
244
- # Otherwise we have an issue where all the records are returned the first time
245
- # the collection is loaded, but on subsequent calls the options_for_find are
246
- # included and you get different results. Note that with options like :order
247
- # this cannot be helped. We don't want to modify the query that generates the
248
- # collection because the idea is to allow getting different perspectives of the
249
- # cached collection without relying on modifying the collection as a whole.
250
- #
251
- # If you do want a specialized, modified, or subset of the collection it's best
252
- # to define it in a block and have a new cache for it:
253
- #
254
- # model.my_special_collection { the_collection(:order => 'new order', :limit => 10) }
255
- def process_result_in_memory(records)
256
- if opts.include?(:page)
257
- records = records.respond_to?(:to_a) ? records.to_a : records
258
- records.respond_to?(:paginate) ? records.paginate(opts_for_paginate) : records
259
- elsif opts.include?(:limit)
260
- records = records.respond_to?(:to_a) ? records.to_a : records
261
- offset = opts[:offset] || 0
262
- records[offset, opts[:limit]]
263
- else
264
- records
265
- end
266
- end
267
-
268
- def execute_count
269
- get_records
270
- cached = AridCache::CacheProxy::Result.new
271
-
272
- # Just get the count if we can.
273
- #
274
- # Because of how AssociationProxy works, if we even look at it, it'll
275
- # trigger the query. So don't look.
276
- #
277
- # Association proxy or named scope. Check for an association first, because
278
- # it doesn't trigger the select if it's actually named scope. Calling respond_to?
279
- # on an association proxy will hower trigger a select because it loads up the target
280
- # and passes the respond_to? on to it.
281
- if records.respond_to?(:proxy_reflection) || records.respond_to?(:proxy_options)
282
- cached.count = records.count # just get the count
283
- cached.klass = object_base_class
284
- elsif records.is_a?(Enumerable) && (records.empty? || records.first.is_a?(::ActiveRecord::Base))
285
- cached.ids = records.collect(&:id) # get everything now that we have it
286
- cached.count = records.size
287
- cached.klass = records.empty? ? object_base_class : records.first.class
288
- else
289
- cached = records # some base type, cache it as itself
290
- end
291
-
292
- Rails.cache.write(cache_key, cached, opts_for_cache)
293
- self.cached = cached
294
- cached.respond_to?(:count) ? cached.count : cached
295
- end
296
-
297
- OPTIONS_FOR_PAGINATE = [:page, :per_page, :total_entries, :finder]
298
-
299
- # Filter options for paginate, if *klass* is set, we get the :per_page value from it.
300
- def opts_for_paginate
301
- paginate_opts = combined_options.reject { |k,v| !OPTIONS_FOR_PAGINATE.include?(k) }
302
- paginate_opts[:finder] = :find_all_by_id unless paginate_opts.include?(:finder)
303
- paginate_opts[:per_page] = klass.per_page if klass && !paginate_opts.include?(:per_page)
304
- paginate_opts
305
- end
306
-
307
- OPTIONS_FOR_FIND = [ :conditions, :include, :joins, :limit, :offset, :order, :select, :readonly, :group, :having, :from, :lock ]
308
-
309
- # Preserve the original order of the results if no :order option is specified.
310
- #
311
- # @arg ids array of ids to order by unless an :order option is specified. If not
312
- # specified, cached.ids is used.
313
- def opts_for_find(ids=nil)
314
- ids ||= cached.ids
315
- find_opts = combined_options.reject { |k,v| !OPTIONS_FOR_FIND.include?(k) }
316
- find_opts[:order] = preserve_order(ids) unless find_opts.include?(:order)
317
- find_opts
318
- end
319
-
320
- OPTIONS_FOR_CACHE = [ :expires_in ]
321
-
322
- def opts_for_cache
323
- combined_options.reject { |k,v| !OPTIONS_FOR_CACHE.include?(k) }
324
- end
325
-
326
- OPTIONS_FOR_CACHE_KEY = [ :auto_expire ]
327
-
328
- def opts_for_cache_key
329
- combined_options.reject { |k,v| !OPTIONS_FOR_CACHE_KEY.include?(k) }
330
- end
331
-
332
- OPTIONS_FOR_CACHE_PROXY = [:raw, :clear]
333
-
334
- # Returns options that affect the cache proxy result
335
- def opts_for_cache_proxy
336
- combined_options.reject { |k,v| !OPTIONS_FOR_CACHE_PROXY.include?(k) }
337
- end
338
-
339
- def object_base_class #:nodoc:
340
- object.is_a?(Class) ? object : object.class
341
- end
342
-
343
- # Generate an ORDER BY clause that preserves the ordering of the ids in *ids*.
344
- #
345
- # The method we use depends on the database adapter because only MySQL
346
- # supports the ORDER BY FIELD() function. For other databases we use
347
- # a CASE statement.
348
- #
349
- # TODO: is it quicker to sort in memory?
350
- def preserve_order(ids)
351
- column = if self.klass.respond_to?(:table_name)
352
- ::ActiveRecord::Base.connection.quote_table_name(self.klass.table_name) + '.id'
353
- else
354
- "id"
355
- end
356
-
357
- if ids.empty?
358
- nil
359
- elsif ::ActiveRecord::Base.is_mysql_adapter?
360
- "FIELD(#{column},#{ids.join(',')})"
361
- else
362
- order = ''
363
- ids.each_index { |i| order << "WHEN #{column}=#{ids[i]} THEN #{i+1} " }
364
- "CASE " + order + " END"
365
- end
123
+ def write_cache(data)
124
+ Rails.cache.write(@cache_key, data, @options.opts_for_cache)
366
125
  end
367
126
  end
368
127
  end
data/lib/arid_cache.rb CHANGED
@@ -10,7 +10,7 @@ require 'arid_cache/inflector'
10
10
 
11
11
  module AridCache
12
12
  extend AridCache::Helpers
13
- class Error < StandardError; end #:nodoc:
13
+ Error = Class.new(StandardError) #:nodoc:
14
14
 
15
15
  def self.cache
16
16
  AridCache::CacheProxy
@@ -1,9 +1,9 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe AridCache::CacheProxy::Result do
3
+ describe AridCache::CacheProxy::CachedResult do
4
4
  before :each do
5
5
  class X; end
6
- @result = AridCache::CacheProxy::Result.new
6
+ @result = AridCache::CacheProxy::CachedResult.new
7
7
  end
8
8
 
9
9
  it "should set the klass from a class" do
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ describe AridCache::CacheProxy::Options do
4
+ def new_options(opts={})
5
+ AridCache::CacheProxy::Options.new(opts)
6
+ end
7
+
8
+ describe "defaults" do
9
+ before :each do
10
+ @opt = new_options
11
+ end
12
+
13
+ it "should have default" do
14
+ @opt.force?.should be_false
15
+ @opt.paginate?.should be_false
16
+ @opt.raw?.should be_false
17
+ @opt.count_only?.should be_false
18
+ @opt.order_by_proc?.should be_false
19
+ @opt.order_by_key?.should be_false
20
+ end
21
+ end
22
+
23
+ it "force?" do
24
+ new_options(:force => true).force?.should be_true
25
+ end
26
+
27
+ it "paginate?" do
28
+ new_options(:page => 1).paginate?.should be_true
29
+ new_options(:per_page => 1).paginate?.should be_false
30
+ new_options(:page => 1, :per_page => 1).paginate?.should be_true
31
+ end
32
+
33
+ it "raw?" do
34
+ new_options(:raw => true).raw?.should be_true
35
+ end
36
+
37
+ it "count_only?" do
38
+ new_options(:count_only => true).count_only?.should be_true
39
+ end
40
+
41
+ it "order options" do
42
+ @opt = new_options(:order => 'key')
43
+ @opt.order_by_key?.should be_true
44
+ @opt.order_by_proc?.should be_false
45
+ @opt = new_options(:order => :symbol)
46
+ @opt.order_by_key?.should be_true
47
+ @opt.order_by_proc?.should be_false
48
+ @opt = new_options(:order => Proc.new {})
49
+ @opt.order_by_key?.should be_false
50
+ @opt.order_by_proc?.should be_true
51
+ end
52
+
53
+ describe "options for paginate" do
54
+ before :each do
55
+ @result_klass = Class.new do
56
+ def self.per_page; 23; end
57
+ end
58
+ end
59
+
60
+ it "should get per_page from the result_klass" do
61
+ @opts = new_options(:result_klass => @result_klass).opts_for_paginate
62
+ @opts[:per_page].should == 23
63
+ end
64
+
65
+ it "should use the provided per_page value" do
66
+ @opts = new_options(:result_klass => @result_klass, :per_page => 3).opts_for_paginate
67
+ @opts[:per_page].should == 3
68
+ end
69
+
70
+ it "should set total_entries" do
71
+ new_options.opts_for_paginate[:total_entries].should be_nil
72
+ @opts = new_options.opts_for_paginate((1..10).to_a)
73
+ @opts[:total_entries].should == 10
74
+ end
75
+
76
+ it "should use find_all_by_id as the finder" do
77
+ new_options.opts_for_paginate[:finder].should == :find_all_by_id
78
+ end
79
+ end
80
+ end