arid_cache 0.1.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  module AridCache
2
2
  class CacheProxy
3
- attr_accessor :object, :key, :opts, :blueprint, :cached, :cache_key, :block, :records
3
+ attr_accessor :object, :key, :opts, :blueprint, :cached, :cache_key, :block, :records, :combined_options
4
4
 
5
5
  # AridCache::CacheProxy::Result
6
6
  #
@@ -17,7 +17,7 @@ module AridCache
17
17
  end
18
18
 
19
19
  def klass=(value)
20
- self['klass'] = value.is_a?(Class) ? value.name : value
20
+ self['klass'] = value.is_a?(Class) ? value.name : value.class.name
21
21
  end
22
22
 
23
23
  def klass
@@ -25,7 +25,11 @@ module AridCache
25
25
  end
26
26
  end
27
27
 
28
- def self.clear_all_caches
28
+ #
29
+ # Managing your caches
30
+ #
31
+
32
+ def self.clear_caches
29
33
  Rails.cache.delete_matched(%r[arid-cache-.*])
30
34
  end
31
35
 
@@ -37,12 +41,12 @@ module AridCache
37
41
  def self.clear_instance_caches(object)
38
42
  key = (object.is_a?(Class) ? object : object.class).name.pluralize.downcase
39
43
  Rails.cache.delete_matched(%r[arid-cache-#{key}.*])
40
- end
41
-
42
- def self.has?(object, key)
43
- Rails.cache.exist?(object.arid_cache_key(key))
44
44
  end
45
45
 
46
+ #
47
+ # Fetching results
48
+ #
49
+
46
50
  def self.fetch_count(object, key, opts, &block)
47
51
  CacheProxy.new(object, key, opts, &block).fetch_count
48
52
  end
@@ -54,16 +58,20 @@ module AridCache
54
58
  def initialize(object, key, opts, &block)
55
59
  self.object = object
56
60
  self.key = key
57
- self.opts = opts || {}
61
+ self.opts = opts.symbolize_keys || {}
58
62
  self.blueprint = AridCache.store.find(object, key)
59
- self.cache_key = object.arid_cache_key(key)
60
- self.cached = Rails.cache.read(cache_key)
61
63
  self.block = block
62
64
  self.records = nil
65
+
66
+ # The options from the blueprint merged with the options for this call
67
+ self.combined_options = self.blueprint.nil? ? self.opts : self.blueprint.opts.merge(self.opts)
68
+
69
+ self.cache_key = object.arid_cache_key(key, opts_for_cache_key)
70
+ self.cached = Rails.cache.read(cache_key, opts_for_cache)
63
71
  end
64
72
 
65
73
  def fetch_count
66
- if cached.nil? || opts[:force]
74
+ if refresh_cache?
67
75
  execute_count
68
76
  elsif cached.is_a?(AridCache::CacheProxy::Result)
69
77
  cached.has_count? ? cached.count : execute_count
@@ -77,13 +85,26 @@ module AridCache
77
85
  end
78
86
 
79
87
  def fetch
80
- if cached.nil? || opts[:force]
88
+ if refresh_cache?
81
89
  execute_find
82
90
  elsif cached.is_a?(AridCache::CacheProxy::Result)
83
- if cached.has_ids? # paginate and fetch here
84
- klass = find_class_of_results
85
- if opts.include?(:page)
86
- klass.paginate(cached.ids, opts_for_paginate)
91
+ if cached.has_ids?
92
+ klass = cached.klass || object_base_class
93
+ if combined_options.include?(:page)
94
+ if combined_options.include?(:order) # order and paginate in the database
95
+ klass.paginate(cached.ids, opts_for_find.merge(opts_for_paginate(klass)))
96
+ else # paginate in memory
97
+ paged_ids = cached.ids.paginate(opts_for_paginate(klass))
98
+ paged_ids.replace(klass.find(paged_ids, preserve_order(opts_for_find, paged_ids)))
99
+ end
100
+ elsif combined_options.include?(:limit) || combined_options.include?(:offset)
101
+ if combined_options.include?(:order) # limit and offset in the database
102
+ klass.find(cached.ids, opts_for_find)
103
+ else # limit and offset in memory
104
+ offset, limit = combined_options.delete(:offset) || 0, combined_options.delete(:limit) || cached.count
105
+ ids = cached.ids[offset, limit]
106
+ klass.find(ids, preserve_order(opts_for_find, ids))
107
+ end
87
108
  else
88
109
  klass.find(cached.ids, opts_for_find)
89
110
  end
@@ -97,8 +118,12 @@ module AridCache
97
118
 
98
119
  private
99
120
 
121
+ def refresh_cache?
122
+ cached.nil? || opts[:force]
123
+ end
124
+
100
125
  def get_records
101
- block = block || blueprint.proc
126
+ block = block || (blueprint && blueprint.proc)
102
127
  self.records = block.nil? ? object.instance_eval(key) : object.instance_eval(&block)
103
128
  end
104
129
 
@@ -119,7 +144,7 @@ module AridCache
119
144
  cached.klass = object_base_class
120
145
  end
121
146
  end
122
- Rails.cache.write(cache_key, cached)
147
+ Rails.cache.write(cache_key, cached, opts_for_cache)
123
148
 
124
149
  self.cached = cached
125
150
  return_records(records)
@@ -142,7 +167,7 @@ module AridCache
142
167
  # If you do want a specialized, modified, or subset of the collection it's best
143
168
  # to define it in a block and have a new cache for it:
144
169
  #
145
- # model.my_special_collection { the_collection(:order => 'new order') }
170
+ # model.my_special_collection { the_collection(:order => 'new order', :limit => 10) }
146
171
  def return_records(records)
147
172
  if opts.include?(:page)
148
173
  records = records.respond_to?(:to_a) ? records.to_a : records
@@ -180,30 +205,58 @@ module AridCache
180
205
  cached = records # some base type, cache it as itself
181
206
  end
182
207
 
183
- Rails.cache.write(cache_key, cached)
208
+ Rails.cache.write(cache_key, cached, opts_for_cache)
184
209
  self.cached = cached
185
210
  cached.respond_to?(:count) ? cached.count : cached
186
211
  end
187
-
188
- def opts_for_paginate
189
- paginate_opts = blueprint.nil? ? opts.symbolize_keys : blueprint.opts.merge(opts.symbolize_keys)
190
- paginate_opts[:total_entries] = cached.count
212
+
213
+ OPTIONS_FOR_PAGINATE = [:page, :per_page, :total_entries]
214
+
215
+ # @arg klass target class, if supplied and no :per_page option is specified, calls
216
+ # klass.per_page to get it
217
+ def opts_for_paginate(klass = nil)
218
+ paginate_opts = combined_options.reject { |k,v| !OPTIONS_FOR_PAGINATE.include?(k) }
219
+ paginate_opts[:per_page] = klass.per_page if klass && !paginate_opts.include?(:per_page)
191
220
  paginate_opts
192
221
  end
193
222
 
194
- VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :order, :select, :readonly, :group, :having, :from, :lock ]
195
-
223
+ OPTIONS_FOR_FIND = [ :conditions, :include, :joins, :limit, :offset, :order, :select, :readonly, :group, :having, :from, :lock ]
224
+
196
225
  def opts_for_find
197
- find_opts = blueprint.nil? ? opts.symbolize_keys : blueprint.opts.merge(opts.symbolize_keys)
198
- find_opts.delete_if { |k,v| !VALID_FIND_OPTIONS.include?(k) }
226
+ combined_options.reject { |k,v| !OPTIONS_FOR_FIND.include?(k) }
199
227
  end
200
228
 
201
- def object_base_class
202
- object.is_a?(Class) ? object : object.class
229
+ OPTIONS_FOR_CACHE = [ :expires_in ]
230
+
231
+ def opts_for_cache
232
+ combined_options.reject { |k,v| !OPTIONS_FOR_CACHE.include?(k) }
203
233
  end
234
+
235
+ OPTIONS_FOR_CACHE_KEY = [ :auto_expire ]
204
236
 
205
- def find_class_of_results
206
- opts[:class] || (blueprint && blueprint.opts[:class]) || cached.klass || object_base_class
237
+ def opts_for_cache_key
238
+ combined_options.reject { |k,v| !OPTIONS_FOR_CACHE_KEY.include?(k) }
239
+ end
240
+
241
+ def object_base_class
242
+ object.is_a?(Class) ? object : object.class
207
243
  end
244
+
245
+ # Preserve the original order of the results if no :order option is specified.
246
+ # The method we use depends on the database adapter because only MySQL
247
+ # supports the ORDER BY FIELD() function.
248
+ # @arg options hash options for find
249
+ # @arg ids array of ids to order by
250
+ def preserve_order(options, ids)
251
+ return options if options.include?(:order)
252
+ if ::ActiveRecord::Base.is_mysql_adapter?
253
+ options[:order] = "FIELD(id,#{ids.join(',')})"
254
+ else
255
+ order = ''
256
+ ids.each_index { |i| order << "WHEN id=#{ids[i]} THEN #{i+1} " }
257
+ options[:order] = "CASE " + order + " END"
258
+ end
259
+ options
260
+ end
208
261
  end
209
262
  end
@@ -23,7 +23,9 @@ module AridCache
23
23
  define(object, $1, opts, :fetch_count, key)
24
24
  else
25
25
  raise ArgumentError.new("#{object} doesn't respond to #{key} or #{$1}! Cannot dynamically create query to get the count, please call with a block.")
26
- end
26
+ end
27
+ elsif AridCache.store.has?(object, key)
28
+ method_for_cached(object, key, :fetch)
27
29
  elsif object.respond_to?(key)
28
30
  define(object, key, opts, &block)
29
31
  else
@@ -38,9 +40,6 @@ module AridCache
38
40
  #
39
41
  # @return an AridCache::Store::Blueprint.
40
42
  def define(object, key, opts, fetch_method=:fetch, method_name=nil, &block)
41
- if block.nil? && !object.respond_to?(key)
42
- raise ArgumentError.new("#{object} doesn't respond to #{key}! Cannot dynamically create a block for your cache item.")
43
- end
44
43
 
45
44
  # FIXME: Pass default options to store.add
46
45
  # Pass nil in for now until we get the cache_ calls working.
@@ -57,7 +56,7 @@ module AridCache
57
56
  #
58
57
  # When the cache_ methods are supported, those options should be
59
58
  # remembered and applied to the collection however.
60
- blueprint = AridCache.store.add(object, key, block, nil)
59
+ blueprint = AridCache.store.add_object_cache_configuration(object, key, nil, block)
61
60
  method_for_cached(object, key, fetch_method, method_name)
62
61
  blueprint
63
62
  end
@@ -1,22 +1,21 @@
1
1
  module AridCache
2
2
  class Store < Hash
3
- extend ActiveSupport::Memoizable
4
3
 
5
4
  # AridCache::Store::Blueprint
6
5
  #
7
6
  # Stores options and blocks that are used to generate results for finds
8
7
  # and counts.
9
- Blueprint = Struct.new(:key, :klass, :proc, :opts) do
8
+ Blueprint = Struct.new(:klass, :key, :opts, :proc) do
10
9
 
11
- def initialize(key, klass, proc=nil, opts={})
10
+ def initialize(klass, key, opts={}, proc=nil)
12
11
  self.key = key
13
12
  self.klass = klass
14
13
  self.proc = proc
15
14
  self.opts = opts
16
15
  end
17
16
 
18
- def klass=(value) # store the base class of *value*
19
- self['klass'] = value.is_a?(Class) ? value.name : value.class.name
17
+ def klass=(object) # store the class name only
18
+ self['klass'] = object.is_a?(Class) ? object.name : object.class.name
20
19
  end
21
20
 
22
21
  def klass
@@ -40,6 +39,35 @@ module AridCache
40
39
  end
41
40
  end
42
41
 
42
+ #
43
+ # Capture cache configurations and blocks and store them in the store.
44
+ #
45
+ # A block is evaluated within the scope of this class. The blocks
46
+ # contains calls to methods which define the caches and give options
47
+ # for each cache.
48
+ #
49
+ # Don't instantiate directly. Rather instantiate the Instance- or
50
+ # ClassCacheConfiguration.
51
+ Configuration = Struct.new(:klass, :global_opts) do
52
+
53
+ def initialize(klass, global_opts={})
54
+ self.global_opts = global_opts
55
+ self.klass = klass
56
+ end
57
+
58
+ def method_missing(key, *args, &block)
59
+ opts = global_opts.merge(args.empty? ? {} : args.first)
60
+ case self
61
+ when InstanceCacheConfiguration
62
+ AridCache.store.add_instance_cache_configuration(klass, key, opts, block)
63
+ else
64
+ AridCache.store.add_class_cache_configuration(klass, key, opts, block)
65
+ end
66
+ end
67
+ end
68
+ class InstanceCacheConfiguration < Configuration; end
69
+ class ClassCacheConfiguration < Configuration; end
70
+
43
71
  def has?(object, key)
44
72
  self.include?(object_store_key(object, key))
45
73
  end
@@ -57,17 +85,27 @@ module AridCache
57
85
  self[object_store_key(object, key)]
58
86
  end
59
87
 
60
- def add(object, key, proc, opts)
88
+ def add_instance_cache_configuration(klass, key, opts, proc)
89
+ store_key = instance_store_key(klass, key)
90
+ self[store_key] = AridCache::Store::Blueprint.new(klass, key, opts, proc)
91
+ end
92
+
93
+ def add_class_cache_configuration(klass, key, opts, proc)
94
+ store_key = class_store_key(klass, key)
95
+ self[store_key] = AridCache::Store::Blueprint.new(klass, key, opts, proc)
96
+ end
97
+
98
+ def add_object_cache_configuration(object, key, opts, proc)
61
99
  store_key = object_store_key(object, key)
62
- self[store_key] = AridCache::Store::Blueprint.new(key, object, proc, opts)
100
+ self[store_key] = AridCache::Store::Blueprint.new(object, key, opts, proc)
63
101
  end
64
-
102
+
65
103
  def find_or_create(object, key)
66
104
  store_key = object_store_key(object, key)
67
105
  if self.include?(store_key)
68
106
  self[store_key]
69
107
  else
70
- self[store_key] = AridCache::Store::Blueprint.new(key, object)
108
+ self[store_key] = AridCache::Store::Blueprint.new(object, key)
71
109
  end
72
110
  end
73
111
 
@@ -76,9 +114,10 @@ module AridCache
76
114
  def initialize
77
115
  end
78
116
 
117
+ def class_store_key(klass, key); klass.name.downcase + '-' + key.to_s; end
118
+ def instance_store_key(klass, key); klass.name.downcase.pluralize + '-' + key.to_s; end
79
119
  def object_store_key(object, key)
80
- (object.is_a?(Class) ? object.name.downcase : object.class.name.pluralize.downcase) + '-' + key.to_s
120
+ case object; when Class; class_store_key(object, key); else; instance_store_key(object.class, key); end
81
121
  end
82
- memoize :object_store_key
83
122
  end
84
123
  end
data/lib/arid_cache.rb CHANGED
@@ -14,8 +14,8 @@ module AridCache
14
14
  AridCache::CacheProxy
15
15
  end
16
16
 
17
- def self.clear_all_caches
18
- AridCache::CacheProxy.clear_all_caches
17
+ def self.clear_caches
18
+ AridCache::CacheProxy.clear_caches
19
19
  end
20
20
 
21
21
  def self.clear_class_caches(object)
@@ -37,7 +37,7 @@ module AridCache
37
37
  base.send(:include, AridCache::ActiveRecord)
38
38
  end
39
39
 
40
- # Initializes ARID Cache for Rails.
40
+ # Initializes AridCache for Rails.
41
41
  #
42
42
  # This method is called by `init.rb`,
43
43
  # which is run by Rails on startup.