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.
- checksums.yaml +15 -0
- data/lib/record_cache.rb +2 -1
- data/lib/record_cache/base.rb +63 -22
- data/lib/record_cache/datastore/active_record.rb +5 -3
- data/lib/record_cache/datastore/active_record_30.rb +95 -38
- data/lib/record_cache/datastore/active_record_31.rb +157 -54
- data/lib/record_cache/datastore/active_record_32.rb +444 -0
- data/lib/record_cache/dispatcher.rb +47 -47
- data/lib/record_cache/multi_read.rb +14 -1
- data/lib/record_cache/query.rb +36 -25
- data/lib/record_cache/statistics.rb +5 -5
- data/lib/record_cache/strategy/base.rb +49 -19
- data/lib/record_cache/strategy/full_table_cache.rb +81 -0
- data/lib/record_cache/strategy/index_cache.rb +38 -36
- data/lib/record_cache/strategy/unique_index_cache.rb +130 -0
- data/lib/record_cache/strategy/util.rb +12 -12
- data/lib/record_cache/test/resettable_version_store.rb +2 -9
- data/lib/record_cache/version.rb +1 -1
- data/lib/record_cache/version_store.rb +23 -16
- data/spec/db/schema.rb +12 -0
- data/spec/db/seeds.rb +10 -0
- data/spec/lib/active_record/visitor_spec.rb +22 -0
- data/spec/lib/base_spec.rb +21 -0
- data/spec/lib/dispatcher_spec.rb +24 -46
- data/spec/lib/multi_read_spec.rb +6 -6
- data/spec/lib/query_spec.rb +43 -43
- data/spec/lib/statistics_spec.rb +28 -28
- data/spec/lib/strategy/base_spec.rb +98 -87
- data/spec/lib/strategy/full_table_cache_spec.rb +68 -0
- data/spec/lib/strategy/index_cache_spec.rb +112 -69
- data/spec/lib/strategy/query_cache_spec.rb +83 -0
- data/spec/lib/strategy/unique_index_on_id_cache_spec.rb +317 -0
- data/spec/lib/strategy/unique_index_on_string_cache_spec.rb +168 -0
- data/spec/lib/strategy/util_spec.rb +67 -49
- data/spec/lib/version_store_spec.rb +22 -41
- data/spec/models/address.rb +9 -0
- data/spec/models/apple.rb +1 -1
- data/spec/models/banana.rb +21 -2
- data/spec/models/language.rb +5 -0
- data/spec/models/person.rb +1 -1
- data/spec/models/store.rb +2 -1
- data/spec/spec_helper.rb +7 -4
- data/spec/support/after_commit.rb +2 -0
- data/spec/support/matchers/hit_cache_matcher.rb +10 -6
- data/spec/support/matchers/log.rb +45 -0
- data/spec/support/matchers/miss_cache_matcher.rb +10 -6
- data/spec/support/matchers/use_cache_matcher.rb +10 -6
- metadata +156 -161
- data/lib/record_cache/strategy/id_cache.rb +0 -93
- data/lib/record_cache/strategy/request_cache.rb +0 -49
- data/spec/lib/strategy/id_cache_spec.rb +0 -168
- 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
|
-
@
|
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
|
-
#
|
17
|
-
def
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
#
|
23
|
-
|
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 [](
|
32
|
-
@
|
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)
|
41
|
-
def fetch(query)
|
42
|
-
|
43
|
-
|
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
|
-
@
|
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
|
-
@
|
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
|
-
#
|
75
|
-
def
|
76
|
-
|
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
|
-
#
|
80
|
-
def
|
81
|
-
@ordered_strategies
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|