record-cache 0.1.2 → 0.1.3

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.
Files changed (52) hide show
  1. checksums.yaml +15 -0
  2. data/lib/record_cache.rb +2 -1
  3. data/lib/record_cache/base.rb +63 -22
  4. data/lib/record_cache/datastore/active_record.rb +5 -3
  5. data/lib/record_cache/datastore/active_record_30.rb +95 -38
  6. data/lib/record_cache/datastore/active_record_31.rb +157 -54
  7. data/lib/record_cache/datastore/active_record_32.rb +444 -0
  8. data/lib/record_cache/dispatcher.rb +47 -47
  9. data/lib/record_cache/multi_read.rb +14 -1
  10. data/lib/record_cache/query.rb +36 -25
  11. data/lib/record_cache/statistics.rb +5 -5
  12. data/lib/record_cache/strategy/base.rb +49 -19
  13. data/lib/record_cache/strategy/full_table_cache.rb +81 -0
  14. data/lib/record_cache/strategy/index_cache.rb +38 -36
  15. data/lib/record_cache/strategy/unique_index_cache.rb +130 -0
  16. data/lib/record_cache/strategy/util.rb +12 -12
  17. data/lib/record_cache/test/resettable_version_store.rb +2 -9
  18. data/lib/record_cache/version.rb +1 -1
  19. data/lib/record_cache/version_store.rb +23 -16
  20. data/spec/db/schema.rb +12 -0
  21. data/spec/db/seeds.rb +10 -0
  22. data/spec/lib/active_record/visitor_spec.rb +22 -0
  23. data/spec/lib/base_spec.rb +21 -0
  24. data/spec/lib/dispatcher_spec.rb +24 -46
  25. data/spec/lib/multi_read_spec.rb +6 -6
  26. data/spec/lib/query_spec.rb +43 -43
  27. data/spec/lib/statistics_spec.rb +28 -28
  28. data/spec/lib/strategy/base_spec.rb +98 -87
  29. data/spec/lib/strategy/full_table_cache_spec.rb +68 -0
  30. data/spec/lib/strategy/index_cache_spec.rb +112 -69
  31. data/spec/lib/strategy/query_cache_spec.rb +83 -0
  32. data/spec/lib/strategy/unique_index_on_id_cache_spec.rb +317 -0
  33. data/spec/lib/strategy/unique_index_on_string_cache_spec.rb +168 -0
  34. data/spec/lib/strategy/util_spec.rb +67 -49
  35. data/spec/lib/version_store_spec.rb +22 -41
  36. data/spec/models/address.rb +9 -0
  37. data/spec/models/apple.rb +1 -1
  38. data/spec/models/banana.rb +21 -2
  39. data/spec/models/language.rb +5 -0
  40. data/spec/models/person.rb +1 -1
  41. data/spec/models/store.rb +2 -1
  42. data/spec/spec_helper.rb +7 -4
  43. data/spec/support/after_commit.rb +2 -0
  44. data/spec/support/matchers/hit_cache_matcher.rb +10 -6
  45. data/spec/support/matchers/log.rb +45 -0
  46. data/spec/support/matchers/miss_cache_matcher.rb +10 -6
  47. data/spec/support/matchers/use_cache_matcher.rb +10 -6
  48. metadata +156 -161
  49. data/lib/record_cache/strategy/id_cache.rb +0 -93
  50. data/lib/record_cache/strategy/request_cache.rb +0 -49
  51. data/spec/lib/strategy/id_cache_spec.rb +0 -168
  52. data/spec/lib/strategy/request_cache_spec.rb +0 -85
@@ -0,0 +1,444 @@
1
+ module RecordCache
2
+ module ActiveRecord
3
+
4
+ module Base
5
+ class << self
6
+ def included(klass)
7
+ klass.extend ClassMethods
8
+ klass.class_eval do
9
+ class << self
10
+ alias_method_chain :find_by_sql, :record_cache
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ # the tests are always run within a transaction, so the threshold is one higher
18
+ RC_TRANSACTIONS_THRESHOLD = ENV['RAILS_ENV'] == 'test' ? 1 : 0
19
+
20
+ # add cache invalidation hooks on initialization
21
+ def record_cache_init
22
+ after_commit :record_cache_create, :on => :create, :prepend => true
23
+ after_commit :record_cache_update, :on => :update, :prepend => true
24
+ after_commit :record_cache_destroy, :on => :destroy, :prepend => true
25
+ end
26
+
27
+ # Retrieve the records, possibly from cache
28
+ def find_by_sql_with_record_cache(sql, binds = [])
29
+ # shortcut, no caching please
30
+ return find_by_sql_without_record_cache(sql, binds) unless record_cache?
31
+
32
+ # check the piggy-back'd ActiveRelation record to see if the query can be retrieved from cache
33
+ arel = sql.is_a?(String) ? sql.instance_variable_get(:@arel) : sql
34
+
35
+ sanitized_sql = sanitize_sql(sql)
36
+ sanitized_sql = connection.to_sql(sanitized_sql, binds) if sanitized_sql.respond_to?(:ast)
37
+
38
+ records = if connection.query_cache_enabled
39
+ query_cache = connection.instance_variable_get(:@query_cache)
40
+ query_cache["rc/#{sanitized_sql}"][binds] ||= try_record_cache(arel, sanitized_sql, binds)
41
+ elsif connection.open_transactions > RC_TRANSACTIONS_THRESHOLD
42
+ connection.send(:select, sanitized_sql, "#{name} Load", binds)
43
+ else
44
+ try_record_cache(arel, sanitized_sql, binds)
45
+ end
46
+ records.collect! { |record| instantiate(record) } if records[0].is_a?(Hash)
47
+
48
+ records
49
+ end
50
+
51
+ def try_record_cache(arel, sql, binds)
52
+ query = arel && arel.respond_to?(:ast) ? RecordCache::Arel::QueryVisitor.new(binds).accept(arel.ast) : nil
53
+ record_cache.fetch(query) do
54
+ connection.send(:select, sql, "#{name} Load", binds)
55
+ end
56
+ end
57
+
58
+ end
59
+
60
+ end
61
+ end
62
+
63
+ module Arel
64
+
65
+ # The method <ActiveRecord::Base>.find_by_sql is used to actually
66
+ # retrieve the data from the DB.
67
+ # Unfortunately the ActiveRelation record is not accessible from
68
+ # there, so it is piggy-back'd in the SQL string.
69
+ module TreeManager
70
+ def self.included(klass)
71
+ klass.extend ClassMethods
72
+ klass.send(:include, InstanceMethods)
73
+ klass.class_eval do
74
+ alias_method_chain :to_sql, :record_cache
75
+ end
76
+ end
77
+
78
+ module ClassMethods
79
+ end
80
+
81
+ module InstanceMethods
82
+ def to_sql_with_record_cache
83
+ sql = to_sql_without_record_cache
84
+ sql.instance_variable_set(:@arel, self)
85
+ sql
86
+ end
87
+ end
88
+ end
89
+
90
+ # Visitor for the ActiveRelation to extract a simple cache query
91
+ # Only accepts single select queries with equality where statements
92
+ # Rejects queries with grouping / having / offset / etc.
93
+ class QueryVisitor < ::Arel::Visitors::Visitor
94
+ DESC = "DESC".freeze
95
+ COMMA = ",".freeze
96
+
97
+ def initialize(bindings)
98
+ super()
99
+ @bindings = (bindings || []).inject({}){ |h, cv| column, value = cv; h[column.name] = value; h}
100
+ @cacheable = true
101
+ @query = ::RecordCache::Query.new
102
+ end
103
+
104
+ def accept ast
105
+ super
106
+ @cacheable && !ast.lock ? @query : nil
107
+ end
108
+
109
+ private
110
+
111
+ def not_cacheable o
112
+ @cacheable = false
113
+ end
114
+
115
+ def skip o
116
+ end
117
+
118
+ alias :visit_Arel_Nodes_TableAlias :not_cacheable
119
+
120
+ alias :visit_Arel_Nodes_Lock :not_cacheable
121
+
122
+ alias :visit_Arel_Nodes_Sum :not_cacheable
123
+ alias :visit_Arel_Nodes_Max :not_cacheable
124
+ alias :visit_Arel_Nodes_Min :not_cacheable
125
+ alias :visit_Arel_Nodes_Avg :not_cacheable
126
+ alias :visit_Arel_Nodes_Count :not_cacheable
127
+ alias :visit_Arel_Nodes_Addition :not_cacheable
128
+ alias :visit_Arel_Nodes_Subtraction :not_cacheable
129
+ alias :visit_Arel_Nodes_Multiplication :not_cacheable
130
+ alias :visit_Arel_Nodes_NamedFunction :not_cacheable
131
+
132
+ alias :visit_Arel_Nodes_Bin :not_cacheable
133
+ alias :visit_Arel_Nodes_Distinct :not_cacheable
134
+ alias :visit_Arel_Nodes_DistinctOn :not_cacheable
135
+ alias :visit_Arel_Nodes_Division :not_cacheable
136
+ alias :visit_Arel_Nodes_Except :not_cacheable
137
+ alias :visit_Arel_Nodes_Exists :not_cacheable
138
+ alias :visit_Arel_Nodes_InfixOperation :not_cacheable
139
+ alias :visit_Arel_Nodes_Intersect :not_cacheable
140
+ alias :visit_Arel_Nodes_Union :not_cacheable
141
+ alias :visit_Arel_Nodes_UnionAll :not_cacheable
142
+ alias :visit_Arel_Nodes_With :not_cacheable
143
+ alias :visit_Arel_Nodes_WithRecursive :not_cacheable
144
+
145
+ def visit_Arel_Nodes_JoinSource o
146
+ # left and right are array, but using blank as it also works for nil
147
+ @cacheable = o.left.blank? || o.right.blank?
148
+ end
149
+
150
+ alias :visit_Arel_Nodes_CurrentRow :not_cacheable
151
+ alias :visit_Arel_Nodes_Extract :not_cacheable
152
+ alias :visit_Arel_Nodes_Following :not_cacheable
153
+ alias :visit_Arel_Nodes_NamedWindow :not_cacheable
154
+ alias :visit_Arel_Nodes_Over :not_cacheable
155
+ alias :visit_Arel_Nodes_Preceding :not_cacheable
156
+ alias :visit_Arel_Nodes_Range :not_cacheable
157
+ alias :visit_Arel_Nodes_Rows :not_cacheable
158
+ alias :visit_Arel_Nodes_Window :not_cacheable
159
+
160
+ alias :visit_Arel_Nodes_As :skip
161
+ alias :visit_Arel_Nodes_Ascending :skip
162
+ alias :visit_Arel_Nodes_Descending :skip
163
+ alias :visit_Arel_Nodes_False :skip
164
+ alias :visit_Arel_Nodes_True :skip
165
+
166
+ alias :visit_Arel_Nodes_StringJoin :not_cacheable
167
+ alias :visit_Arel_Nodes_InnerJoin :not_cacheable
168
+ alias :visit_Arel_Nodes_OuterJoin :not_cacheable
169
+
170
+ alias :visit_Arel_Nodes_DeleteStatement :not_cacheable
171
+ alias :visit_Arel_Nodes_InsertStatement :not_cacheable
172
+ alias :visit_Arel_Nodes_UpdateStatement :not_cacheable
173
+
174
+
175
+ alias :unary :not_cacheable
176
+ alias :visit_Arel_Nodes_Group :unary
177
+ alias :visit_Arel_Nodes_Having :unary
178
+ alias :visit_Arel_Nodes_Not :unary
179
+ alias :visit_Arel_Nodes_On :unary
180
+ alias :visit_Arel_Nodes_UnqualifiedColumn :unary
181
+
182
+ def visit_Arel_Nodes_Offset o
183
+ @cacheable = false unless o.expr == 0
184
+ end
185
+
186
+ def visit_Arel_Nodes_Values o
187
+ visit o.expressions if @cacheable
188
+ end
189
+
190
+ def visit_Arel_Nodes_Limit o
191
+ @query.limit = o.expr
192
+ end
193
+ alias :visit_Arel_Nodes_Top :visit_Arel_Nodes_Limit
194
+
195
+ GROUPING_EQUALS_REGEXP = /^\W?(\w*)\W?\.\W?(\w*)\W?\s*=\s*(\d+)$/ # `calendars`.account_id = 5
196
+ GROUPING_IN_REGEXP = /^^\W?(\w*)\W?\.\W?(\w*)\W?\s*IN\s*\(([\d\s,]+)\)$/ # `service_instances`.`id` IN (118,80,120,82)
197
+ def visit_Arel_Nodes_Grouping o
198
+ return unless @cacheable
199
+ if @table_name && o.expr =~ GROUPING_EQUALS_REGEXP && $1 == @table_name
200
+ @cacheable = @query.where($2, $3.to_i)
201
+ elsif @table_name && o.expr =~ GROUPING_IN_REGEXP && $1 == @table_name
202
+ @cacheable = @query.where($2, $3.split(',').map(&:to_i))
203
+ else
204
+ @cacheable = false
205
+ end
206
+ end
207
+
208
+ def visit_Arel_Nodes_SelectCore o
209
+ @cacheable = false unless o.groups.empty?
210
+ visit o.froms if @cacheable
211
+ visit o.wheres if @cacheable
212
+ visit o.source if @cacheable
213
+ # skip o.projections
214
+ end
215
+
216
+ def visit_Arel_Nodes_SelectStatement o
217
+ @cacheable = false if o.cores.size > 1
218
+ if @cacheable
219
+ visit o.offset
220
+ o.orders.map { |x| handle_order_by(visit x) } if @cacheable && o.orders.size > 0
221
+ visit o.limit
222
+ visit o.cores
223
+ end
224
+ end
225
+
226
+ ORDER_BY_REGEXP = /^\s*([\w\.]*)\s*(|ASC|asc|DESC|desc)\s*$/ # people.id DESC
227
+ def handle_order_by(order)
228
+ order.to_s.split(COMMA).each do |o|
229
+ # simple sort order (+people.id+ can be replaced by +id+, as joins are not allowed anyways)
230
+ if o.match(ORDER_BY_REGEXP)
231
+ asc = $2.upcase == DESC ? false : true
232
+ @query.order_by($1.split('.').last, asc)
233
+ else
234
+ @cacheable = false
235
+ end
236
+ end
237
+ end
238
+
239
+ def visit_Arel_Table o
240
+ @table_name = o.name
241
+ end
242
+
243
+ def visit_Arel_Attributes_Attribute o
244
+ o.name.to_sym
245
+ end
246
+ alias :visit_Arel_Attributes_Integer :visit_Arel_Attributes_Attribute
247
+ alias :visit_Arel_Attributes_Float :visit_Arel_Attributes_Attribute
248
+ alias :visit_Arel_Attributes_String :visit_Arel_Attributes_Attribute
249
+ alias :visit_Arel_Attributes_Time :visit_Arel_Attributes_Attribute
250
+ alias :visit_Arel_Attributes_Boolean :visit_Arel_Attributes_Attribute
251
+ alias :visit_Arel_Attributes_Decimal :visit_Arel_Attributes_Attribute
252
+
253
+ def visit_Arel_Nodes_Equality o
254
+ key, value = visit(o.left), visit(o.right)
255
+ # several different binding markers exist depending on the db driver used (MySQL, Postgress supported)
256
+ if value.to_s =~ /^(\?|\u0000|\$\d+)$/
257
+ # puts "bindings: #{@bindings.inspect}, key = #{key.to_s}"
258
+ value = @bindings[key.to_s] || value
259
+ end
260
+ # puts " =====> equality found: #{key.inspect}@#{key.class.name} => #{value.inspect}@#{value.class.name}"
261
+ @query.where(key, value)
262
+ end
263
+ alias :visit_Arel_Nodes_In :visit_Arel_Nodes_Equality
264
+
265
+ def visit_Arel_Nodes_And o
266
+ visit(o.children)
267
+ end
268
+
269
+ alias :visit_Arel_Nodes_Or :not_cacheable
270
+ alias :visit_Arel_Nodes_NotEqual :not_cacheable
271
+ alias :visit_Arel_Nodes_GreaterThan :not_cacheable
272
+ alias :visit_Arel_Nodes_GreaterThanOrEqual :not_cacheable
273
+ alias :visit_Arel_Nodes_Assignment :not_cacheable
274
+ alias :visit_Arel_Nodes_LessThan :not_cacheable
275
+ alias :visit_Arel_Nodes_LessThanOrEqual :not_cacheable
276
+ alias :visit_Arel_Nodes_Between :not_cacheable
277
+ alias :visit_Arel_Nodes_NotIn :not_cacheable
278
+ alias :visit_Arel_Nodes_DoesNotMatch :not_cacheable
279
+ alias :visit_Arel_Nodes_Matches :not_cacheable
280
+
281
+ def visit_Fixnum o
282
+ o.to_i
283
+ end
284
+ alias :visit_Bignum :visit_Fixnum
285
+
286
+ def visit_Symbol o
287
+ o.to_sym
288
+ end
289
+
290
+ def visit_Object o
291
+ o
292
+ end
293
+ alias :visit_Arel_Nodes_SqlLiteral :visit_Object
294
+ alias :visit_Arel_Nodes_BindParam :visit_Object
295
+ alias :visit_Arel_SqlLiteral :visit_Object # This is deprecated
296
+ alias :visit_String :visit_Object
297
+ alias :visit_NilClass :visit_Object
298
+ alias :visit_TrueClass :visit_Object
299
+ alias :visit_FalseClass :visit_Object
300
+ alias :visit_Arel_SqlLiteral :visit_Object
301
+ alias :visit_BigDecimal :visit_Object
302
+ alias :visit_Float :visit_Object
303
+ alias :visit_Time :visit_Object
304
+ alias :visit_Date :visit_Object
305
+ alias :visit_DateTime :visit_Object
306
+ alias :visit_Hash :visit_Object
307
+
308
+ def visit_Array o
309
+ o.map{ |x| visit x }
310
+ end
311
+ end
312
+ end
313
+
314
+ end
315
+
316
+ module RecordCache
317
+
318
+ # Patch ActiveRecord::Relation to make sure update_all will invalidate all referenced records
319
+ module ActiveRecord
320
+ module UpdateAll
321
+ class << self
322
+ def included(klass)
323
+ klass.extend ClassMethods
324
+ klass.send(:include, InstanceMethods)
325
+ klass.class_eval do
326
+ alias_method_chain :update_all, :record_cache
327
+ end
328
+ end
329
+ end
330
+
331
+ module ClassMethods
332
+ end
333
+
334
+ module InstanceMethods
335
+ def __find_in_clause(sub_select)
336
+ return nil unless sub_select.arel.constraints.count == 1
337
+ constraint = sub_select.arel.constraints.first
338
+ return constraint if constraint.is_a?(::Arel::Nodes::In) # directly an IN clause
339
+ return nil unless constraint.respond_to?(:children) && constraint.children.count == 1
340
+ constraint = constraint.children.first
341
+ return constraint if constraint.is_a?(::Arel::Nodes::In) # AND with IN clause
342
+ nil
343
+ end
344
+
345
+ def update_all_with_record_cache(updates, conditions = nil, options = {})
346
+ result = update_all_without_record_cache(updates, conditions, options)
347
+
348
+ if record_cache?
349
+ # when this condition is met, the arel.update method will be called on the current scope, see ActiveRecord::Relation#update_all
350
+ unless conditions || options.present? || @limit_value.present? != @order_values.present?
351
+ # get all attributes that contain a unique index for this model
352
+ unique_index_attributes = RecordCache::Strategy::UniqueIndexCache.attributes(self)
353
+ # go straight to SQL result (without instantiating records) for optimal performance
354
+ RecordCache::Base.version_store.multi do
355
+ sub_select = select(unique_index_attributes.map(&:to_s).join(','))
356
+ in_clause = __find_in_clause(sub_select)
357
+ if unique_index_attributes.size == 1 && in_clause &&
358
+ in_clause.left.try(:name).to_s == unique_index_attributes.first.to_s
359
+ # common case where the unique index is the (only) constraint on the query: SELECT id FROM people WHERE id in (...)
360
+ attribute = unique_index_attributes.first
361
+ in_clause.right.each do |value|
362
+ record_cache.invalidate(attribute, value)
363
+ end
364
+ else
365
+ connection.execute(sub_select.to_sql).each do |row|
366
+ # invalidate the unique index for all attributes
367
+ unique_index_attributes.each_with_index do |attribute, index|
368
+ record_cache.invalidate(attribute, (row.is_a?(Hash) ? row[attribute.to_s] : row[index]))
369
+ end
370
+ end
371
+ end
372
+ end
373
+ end
374
+ end
375
+
376
+ result
377
+ end
378
+ end
379
+ end
380
+ end
381
+
382
+ # Patch ActiveRecord::Associations::HasManyAssociation to make sure the index_cache is updated when records are
383
+ # deleted from the collection
384
+ module ActiveRecord
385
+ module HasMany
386
+ class << self
387
+ def included(klass)
388
+ klass.extend ClassMethods
389
+ klass.send(:include, InstanceMethods)
390
+ klass.class_eval do
391
+ alias_method_chain :delete_records, :record_cache
392
+ end
393
+ end
394
+ end
395
+
396
+ module ClassMethods
397
+ end
398
+
399
+ module InstanceMethods
400
+ def delete_records_with_record_cache(records, method)
401
+ # invalidate :id cache for all records
402
+ records.each{ |record| record.class.record_cache.invalidate(record.id) if record.class.record_cache? unless record.new_record? }
403
+ # invalidate the referenced class for the attribute/value pair on the index cache
404
+ @reflection.klass.record_cache.invalidate(@reflection.foreign_key.to_sym, @owner.id) if @reflection.klass.record_cache?
405
+ delete_records_without_record_cache(records, method)
406
+ end
407
+ end
408
+ end
409
+
410
+ module HasOne
411
+ class << self
412
+ def included(klass)
413
+ klass.extend ClassMethods
414
+ klass.send(:include, InstanceMethods)
415
+ klass.class_eval do
416
+ alias_method_chain :delete, :record_cache
417
+ end
418
+ end
419
+ end
420
+
421
+ module ClassMethods
422
+ end
423
+
424
+ module InstanceMethods
425
+ def delete_with_record_cache(method = options[:dependent])
426
+ # invalidate :id cache for all record
427
+ if load_target
428
+ target.class.record_cache.invalidate(target.id) if target.class.record_cache? unless target.new_record?
429
+ end
430
+ # invalidate the referenced class for the attribute/value pair on the index cache
431
+ @reflection.klass.record_cache.invalidate(@reflection.foreign_key.to_sym, @owner.id) if @reflection.klass.record_cache?
432
+ delete_without_record_cache(method)
433
+ end
434
+ end
435
+ end
436
+ end
437
+
438
+ end
439
+
440
+ ActiveRecord::Base.send(:include, RecordCache::ActiveRecord::Base)
441
+ Arel::TreeManager.send(:include, RecordCache::Arel::TreeManager)
442
+ ActiveRecord::Relation.send(:include, RecordCache::ActiveRecord::UpdateAll)
443
+ ActiveRecord::Associations::HasManyAssociation.send(:include, RecordCache::ActiveRecord::HasMany)
444
+ ActiveRecord::Associations::HasOneAssociation.send(:include, RecordCache::ActiveRecord::HasOne)
@@ -1,51 +1,48 @@
1
1
  module RecordCache
2
-
2
+
3
3
  # Every model that calls cache_records will receive an instance of this class
4
4
  # accessible through +<model>.record_cache+
5
5
  #
6
6
  # The dispatcher is responsible for dispatching queries, record_changes and invalidation calls
7
7
  # to the appropriate cache strategies.
8
8
  class Dispatcher
9
+
10
+ # Retrieve all strategies ordered by fastest strategy first.
11
+ #
12
+ # Roll your own cache strategies by extending from +RecordCache::Strategy::Base+,
13
+ # and registering it here +RecordCache::Dispatcher.strategy_classes << MyStrategy+
14
+ def self.strategy_classes
15
+ @strategy_classes ||= [RecordCache::Strategy::UniqueIndexCache, RecordCache::Strategy::FullTableCache, RecordCache::Strategy::IndexCache]
16
+ end
17
+
9
18
  def initialize(base)
10
19
  @base = base
11
- @strategy_by_id = {}
12
- # all strategies except :request_cache, with the :id stategy first (most used and best performing)
13
- @ordered_strategies = []
20
+ @strategy_by_attribute = {}
14
21
  end
15
22
 
16
- # Register a cache strategy for this model
17
- def register(strategy_id, strategy_klass, record_store, options)
18
- if @strategy_by_id.key?(strategy_id)
19
- return if strategy_id == :id
20
- raise "Multiple record cache definitions found for '#{strategy_id}' on #{@base.name}"
23
+ # Parse the options provided to the cache_records method and create the appropriate cache strategies.
24
+ def parse(options)
25
+ # find the record store, possibly based on the :store option
26
+ store = record_store(options.delete(:store))
27
+ # dispatch the parse call to all known strategies
28
+ Dispatcher.strategy_classes.map{ |klass| klass.parse(@base, store, options) }.flatten.compact.each do |strategy|
29
+ raise "Multiple record cache definitions found for '#{strategy.attribute}' on #{@base.name}" if @strategy_by_attribute[strategy.attribute]
30
+ # and keep track of all strategies
31
+ @strategy_by_attribute[strategy.attribute] = strategy
21
32
  end
22
- # Instantiate the cache strategy
23
- strategy = strategy_klass.new(@base, strategy_id, record_store, options)
24
- # Keep track of all strategies for this model
25
- @strategy_by_id[strategy_id] = strategy
26
- # Note that the :id strategy is always registered first
27
- @ordered_strategies << strategy unless strategy_id == :request_cache
33
+ # make sure the strategies are ordered again on next call to +ordered_strategies+
34
+ @ordered_strategies = nil
28
35
  end
29
36
 
30
37
  # Retrieve the caching strategy for the given attribute
31
- def [](strategy_id)
32
- @strategy_by_id[strategy_id]
33
- end
34
-
35
- # Can the cache retrieve the records based on this query?
36
- def cacheable?(query)
37
- !!first_cacheable_strategy(query)
38
+ def [](attribute)
39
+ @strategy_by_attribute[attribute]
38
40
  end
39
41
 
40
- # retrieve the record(s) with the given id(s) as an array
41
- def fetch(query)
42
- if request_cache
43
- # cache the query in the request
44
- request_cache.fetch(query) { fetch_from_first_cacheable_strategy(query) }
45
- else
46
- # fetch the results using the first strategy that accepts this query
47
- fetch_from_first_cacheable_strategy(query)
48
- end
42
+ # retrieve the record(s) based on the given query (check with cacheable?(query) first)
43
+ def fetch(query, &block)
44
+ strategy = query && ordered_strategies.detect { |strategy| strategy.cacheable?(query) }
45
+ strategy ? strategy.fetch(query) : yield
49
46
  end
50
47
 
51
48
  # Update the version store and the record store (used by callbacks)
@@ -55,35 +52,38 @@ module RecordCache
55
52
  # skip unless something has actually changed
56
53
  return if action == :update && record.previous_changes.empty?
57
54
  # dispatch the record change to all known strategies
58
- @strategy_by_id.values.each { |strategy| strategy.record_change(record, action) }
55
+ @strategy_by_attribute.values.each { |strategy| strategy.record_change(record, action) }
59
56
  end
60
57
 
61
58
  # Explicitly invalidate one or more records
62
- # @param: strategy: the strategy to invalidate
59
+ # @param: strategy: the id of the strategy to invalidate (defaults to +:id+)
63
60
  # @param: value: the value to send to the invalidate method of the chosen strategy
64
61
  def invalidate(strategy, value = nil)
65
62
  (value = strategy; strategy = :id) unless strategy.is_a?(Symbol)
66
63
  # call the invalidate method of the chosen strategy
67
- @strategy_by_id[strategy].invalidate(value) if @strategy_by_id[strategy]
68
- # always clear the request cache if invalidate is explicitly called for this class
69
- request_cache.try(:invalidate, value)
64
+ @strategy_by_attribute[strategy].invalidate(value) if @strategy_by_attribute[strategy]
70
65
  end
71
66
 
72
67
  private
73
68
 
74
- # retrieve the data from the first strategy that handle the query
75
- def fetch_from_first_cacheable_strategy(query)
76
- first_cacheable_strategy(query).fetch(query)
69
+ # Find the cache store for the records (using the :store option)
70
+ def record_store(store)
71
+ store = RecordCache::Base.stores[store] || ActiveSupport::Cache.lookup_store(store) if store.is_a?(Symbol)
72
+ store ||= Rails.cache if defined?(::Rails)
73
+ store ||= ActiveSupport::Cache.lookup_store(:memory_store)
74
+ RecordCache::MultiRead.test(store)
77
75
  end
78
76
 
79
- # find the first strategy that can handle this query
80
- def first_cacheable_strategy(query)
81
- @ordered_strategies.detect { |strategy| strategy.cacheable?(query) }
82
- end
83
-
84
- # retrieve the request cache strategy, if defined for this model
85
- def request_cache
86
- @strategy_by_id[:request_cache]
77
+ # Retrieve all strategies ordered by the fastest strategy first (currently :id, :unique, :index)
78
+ def ordered_strategies
79
+ @ordered_strategies ||= begin
80
+ last_index = Dispatcher.strategy_classes.size
81
+ # sort the strategies based on the +strategy_classes+ index
82
+ ordered = @strategy_by_attribute.values.sort do |x,y|
83
+ (Dispatcher.strategy_classes.index(x.class) || last_index) <=> (Dispatcher.strategy_classes.index(y.class) || last_index)
84
+ end
85
+ ordered
86
+ end
87
87
  end
88
88
 
89
89
  end