record-cache 0.1.3 → 0.1.4

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 (35) hide show
  1. checksums.yaml +8 -8
  2. data/lib/record_cache/base.rb +4 -4
  3. data/lib/record_cache/datastore/active_record_30.rb +1 -1
  4. data/lib/record_cache/datastore/active_record_31.rb +1 -1
  5. data/lib/record_cache/datastore/active_record_32.rb +2 -2
  6. data/lib/record_cache/datastore/active_record_40.rb +445 -0
  7. data/lib/record_cache/datastore/active_record_41.rb +446 -0
  8. data/lib/record_cache/strategy/full_table_cache.rb +1 -1
  9. data/lib/record_cache/strategy/util.rb +20 -3
  10. data/lib/record_cache/version.rb +1 -1
  11. data/spec/db/create-record-cache-db_and_user.sql +5 -0
  12. data/spec/db/database.yml +7 -0
  13. data/spec/db/schema.rb +9 -15
  14. data/spec/initializers/backward_compatibility.rb +32 -0
  15. data/spec/lib/active_record/visitor_spec.rb +1 -1
  16. data/spec/lib/base_spec.rb +2 -2
  17. data/spec/lib/dispatcher_spec.rb +1 -1
  18. data/spec/lib/multi_read_spec.rb +1 -1
  19. data/spec/lib/query_spec.rb +1 -1
  20. data/spec/lib/statistics_spec.rb +1 -1
  21. data/spec/lib/strategy/base_spec.rb +39 -39
  22. data/spec/lib/strategy/full_table_cache_spec.rb +18 -18
  23. data/spec/lib/strategy/index_cache_spec.rb +58 -52
  24. data/spec/lib/strategy/query_cache_spec.rb +1 -1
  25. data/spec/lib/strategy/unique_index_on_id_cache_spec.rb +57 -45
  26. data/spec/lib/strategy/unique_index_on_string_cache_spec.rb +47 -45
  27. data/spec/lib/strategy/util_spec.rb +49 -43
  28. data/spec/lib/version_store_spec.rb +1 -1
  29. data/spec/models/apple.rb +1 -2
  30. data/spec/spec_helper.rb +16 -7
  31. data/spec/support/matchers/hit_cache_matcher.rb +1 -1
  32. data/spec/support/matchers/miss_cache_matcher.rb +1 -1
  33. data/spec/support/matchers/use_cache_matcher.rb +1 -1
  34. metadata +63 -17
  35. data/spec/support/after_commit.rb +0 -73
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- NjcyYzNmZjBjM2VjZGU4MzMzMGI1YjBkNDdiNmQ5NWZiODNiZjE3OA==
4
+ YzM5MTA3YWI2MTRjMDIwMGFmN2FhYzQxZmNlMWU2ZTNiNTkzODkyMQ==
5
5
  data.tar.gz: !binary |-
6
- MzU0ZDM3ZjE1YzhmNTY2MTM3M2MwZmU4ZjZhZTI5ZGQ1NWMwNTMwZg==
6
+ MGYzNGU3NzVlZTI4YTMwZTM5NDhkZDVjZGVjY2UyODJlMWU1YzIzMQ==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- NDE4MTQxYjA2ZDQ2ZTE1ZWVlOTk1MjY5OGU3ZDJjNThjZGQ4MjIxZTE1MWQy
10
- MjMyZmViZmUzZWY2MGU5NDUzOWNhOWFiYWY0MTlmYTAzMTJlOTVjZTA2N2Yz
11
- ODgyNDFiMWY5YzU1MzRhYzNjZWM5M2UyMjU1OGFmNzJlMjliMGI=
9
+ MzQxNDM1ZWQxMmUzYmQyNzZhZjZmNDA2MmQzZWViMWYwZDhlMzI0YTczYjY4
10
+ ZmE4ZDZkNjBkMjc3YjUxNGM1YjJhMDlkNzkwZDRlNGM0MTBlMTUzMTBmZThm
11
+ MDg1NWNjMzZhOGE3OWIxMjc0ODg5YzZmYjY1MmY5MWRkNWFhMDc=
12
12
  data.tar.gz: !binary |-
13
- NDBiNGYyZjIwNDlkMzg5MDU0YjhhNjBjNThiMDhjMzk1YzE1YjgxODUwYTJj
14
- ZTk5YzAzNzY1ZmU4MWI2ZjYyYTU1MjQ4NjNkOTEwZmIxMWMzNzAyYWY5NGJj
15
- YTc2ZDVlN2UyOTZhMzBhY2NjMmUwMWQ0NDI5MjliYjY5MGZjM2Y=
13
+ ZDA2OTRjOTZkODFhMTdiNGU3M2IzZDZiYTljNTViZTdjZGQ2ZjRjODNmY2Ew
14
+ MzA4OTgwYmMyNTE2YzRlZDI4Yjg1MjhiMGFlNTdmYzFmNWZlYWZiYmJlZDc0
15
+ YzMzODM2Y2UyNDIzYjQyNzAwMWY0NWIxZWI0ZjBkNDJlNDVkMTQ=
@@ -9,7 +9,7 @@ module RecordCache
9
9
  module Base
10
10
  class << self
11
11
  def included(klass)
12
- klass.class_eval do
12
+ klass.class_eval do
13
13
  extend ClassMethods
14
14
  include InstanceMethods
15
15
  end
@@ -113,7 +113,7 @@ module RecordCache
113
113
  # :store => the cache store for the instances, e.g. :memory_store, :dalli_store* (default: Rails.cache)
114
114
  # or one of the store ids defined using +RecordCache::Base.register_store+
115
115
  # :key => provide a unique shorter key to limit the cache key length (default: model.name)
116
- #
116
+ #
117
117
  # cache strategy specific options:
118
118
  # :index => one or more attributes (Symbols) for which the ids are cached for the value of the attribute
119
119
  #
@@ -124,7 +124,7 @@ module RecordCache
124
124
  # - use :index => :person_id for aggregated has_many associations
125
125
  def cache_records(options = {})
126
126
  unless @rc_dispatcher
127
- @rc_dispatcher = RecordCache::Dispatcher.new(self)
127
+ @rc_dispatcher = RecordCache::Dispatcher.new(self)
128
128
  # Callback for Data Store specific initialization
129
129
  record_cache_init
130
130
 
@@ -174,4 +174,4 @@ module RecordCache
174
174
  end
175
175
 
176
176
  end
177
- end
177
+ end
@@ -173,7 +173,7 @@ module RecordCache
173
173
  @cacheable = false unless o.groups.empty?
174
174
  visit o.froms if @cacheable
175
175
  visit o.wheres if @cacheable
176
- # skip o.projections
176
+ @cacheable = o.projections.none?{ |projection| projection.to_s =~ /distinct/i } unless o.projections.empty? if @cacheable
177
177
  end
178
178
 
179
179
  def visit_Arel_Nodes_SelectStatement o
@@ -199,7 +199,7 @@ module RecordCache
199
199
  visit o.froms if @cacheable
200
200
  visit o.wheres if @cacheable
201
201
  visit o.source if @cacheable
202
- # skip o.projections
202
+ @cacheable = o.projections.none?{ |projection| projection.to_s =~ /distinct/i } unless o.projections.empty? if @cacheable
203
203
  end
204
204
 
205
205
  def visit_Arel_Nodes_SelectStatement o
@@ -33,7 +33,7 @@ module RecordCache
33
33
  arel = sql.is_a?(String) ? sql.instance_variable_get(:@arel) : sql
34
34
 
35
35
  sanitized_sql = sanitize_sql(sql)
36
- sanitized_sql = connection.to_sql(sanitized_sql, binds) if sanitized_sql.respond_to?(:ast)
36
+ sanitized_sql = connection.to_sql(sanitized_sql, binds.dup) if sanitized_sql.respond_to?(:ast)
37
37
 
38
38
  records = if connection.query_cache_enabled
39
39
  query_cache = connection.instance_variable_get(:@query_cache)
@@ -210,7 +210,7 @@ module RecordCache
210
210
  visit o.froms if @cacheable
211
211
  visit o.wheres if @cacheable
212
212
  visit o.source if @cacheable
213
- # skip o.projections
213
+ @cacheable = o.projections.none?{ |projection| projection.to_s =~ /distinct/i } unless o.projections.empty? if @cacheable
214
214
  end
215
215
 
216
216
  def visit_Arel_Nodes_SelectStatement o
@@ -0,0 +1,445 @@
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, sql, binds)
41
+
42
+ elsif connection.open_transactions > RC_TRANSACTIONS_THRESHOLD
43
+ find_by_sql_without_record_cache(sql, binds)
44
+
45
+ else
46
+ try_record_cache(arel, sql, binds)
47
+ end
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
+ find_by_sql_without_record_cache(sql, 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, attribute
112
+ @cacheable = false
113
+ end
114
+
115
+ def skip o, attribute
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, attribute
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_SelectManager :not_cacheable
162
+ alias :visit_Arel_Nodes_Ascending :skip
163
+ alias :visit_Arel_Nodes_Descending :skip
164
+ alias :visit_Arel_Nodes_False :skip
165
+ alias :visit_Arel_Nodes_True :skip
166
+
167
+ alias :visit_Arel_Nodes_StringJoin :not_cacheable
168
+ alias :visit_Arel_Nodes_InnerJoin :not_cacheable
169
+ alias :visit_Arel_Nodes_OuterJoin :not_cacheable
170
+
171
+ alias :visit_Arel_Nodes_DeleteStatement :not_cacheable
172
+ alias :visit_Arel_Nodes_InsertStatement :not_cacheable
173
+ alias :visit_Arel_Nodes_UpdateStatement :not_cacheable
174
+
175
+
176
+ alias :unary :not_cacheable
177
+ alias :visit_Arel_Nodes_Group :unary
178
+ alias :visit_Arel_Nodes_Having :unary
179
+ alias :visit_Arel_Nodes_Not :unary
180
+ alias :visit_Arel_Nodes_On :unary
181
+ alias :visit_Arel_Nodes_UnqualifiedColumn :unary
182
+
183
+ def visit_Arel_Nodes_Offset o, attribute
184
+ @cacheable = false unless o.expr == 0
185
+ end
186
+
187
+ def visit_Arel_Nodes_Values o, attribute
188
+ visit o.expressions if @cacheable
189
+ end
190
+
191
+ def visit_Arel_Nodes_Limit o, attribute
192
+ @query.limit = o.expr
193
+ end
194
+ alias :visit_Arel_Nodes_Top :visit_Arel_Nodes_Limit
195
+
196
+ GROUPING_EQUALS_REGEXP = /^\W?(\w*)\W?\.\W?(\w*)\W?\s*=\s*(\d+)$/ # `calendars`.account_id = 5
197
+ GROUPING_IN_REGEXP = /^^\W?(\w*)\W?\.\W?(\w*)\W?\s*IN\s*\(([\d\s,]+)\)$/ # `service_instances`.`id` IN (118,80,120,82)
198
+ def visit_Arel_Nodes_Grouping o, attribute
199
+ return unless @cacheable
200
+ if @table_name && o.expr =~ GROUPING_EQUALS_REGEXP && $1 == @table_name
201
+ @cacheable = @query.where($2, $3.to_i)
202
+ elsif @table_name && o.expr =~ GROUPING_IN_REGEXP && $1 == @table_name
203
+ @cacheable = @query.where($2, $3.split(',').map(&:to_i))
204
+ else
205
+ @cacheable = false
206
+ end
207
+ end
208
+
209
+ def visit_Arel_Nodes_SelectCore o, attribute
210
+ @cacheable = false unless o.groups.empty?
211
+ visit o.froms if @cacheable
212
+ visit o.wheres if @cacheable
213
+ visit o.source if @cacheable
214
+ @cacheable = o.projections.none?{ |projection| projection.to_s =~ /distinct/i } unless o.projections.empty? if @cacheable
215
+ end
216
+
217
+ def visit_Arel_Nodes_SelectStatement o, attribute
218
+ @cacheable = false if o.cores.size > 1
219
+ if @cacheable
220
+ visit o.offset
221
+ o.orders.map { |x| handle_order_by(visit x) } if @cacheable && o.orders.size > 0
222
+ visit o.limit
223
+ visit o.cores
224
+ end
225
+ end
226
+
227
+ ORDER_BY_REGEXP = /^\s*([\w\.]*)\s*(|ASC|asc|DESC|desc)\s*$/ # people.id DESC
228
+ def handle_order_by(order)
229
+ order.to_s.split(COMMA).each do |o|
230
+ # simple sort order (+people.id+ can be replaced by +id+, as joins are not allowed anyways)
231
+ if o.match(ORDER_BY_REGEXP)
232
+ asc = $2.upcase == DESC ? false : true
233
+ @query.order_by($1.split('.').last, asc)
234
+ else
235
+ @cacheable = false
236
+ end
237
+ end
238
+ end
239
+
240
+ def visit_Arel_Table o, attribute
241
+ @table_name = o.name
242
+ end
243
+
244
+ def visit_Arel_Attributes_Attribute o, attribute
245
+ o.name.to_sym
246
+ end
247
+ alias :visit_Arel_Attributes_Integer :visit_Arel_Attributes_Attribute
248
+ alias :visit_Arel_Attributes_Float :visit_Arel_Attributes_Attribute
249
+ alias :visit_Arel_Attributes_String :visit_Arel_Attributes_Attribute
250
+ alias :visit_Arel_Attributes_Time :visit_Arel_Attributes_Attribute
251
+ alias :visit_Arel_Attributes_Boolean :visit_Arel_Attributes_Attribute
252
+ alias :visit_Arel_Attributes_Decimal :visit_Arel_Attributes_Attribute
253
+
254
+ def visit_Arel_Nodes_Equality o, attribute
255
+ key, value = visit(o.left), visit(o.right)
256
+ # several different binding markers exist depending on the db driver used (MySQL, Postgress supported)
257
+ if value.to_s =~ /^(\?|\u0000|\$\d+)$/
258
+ # puts "bindings: #{@bindings.inspect}, key = #{key.to_s}"
259
+ value = @bindings[key.to_s] || value
260
+ end
261
+ # puts " =====> equality found: #{key.inspect}@#{key.class.name} => #{value.inspect}@#{value.class.name}"
262
+ @query.where(key, value)
263
+ end
264
+ alias :visit_Arel_Nodes_In :visit_Arel_Nodes_Equality
265
+
266
+ def visit_Arel_Nodes_And o, attribute
267
+ visit(o.children)
268
+ end
269
+
270
+ alias :visit_Arel_Nodes_Or :not_cacheable
271
+ alias :visit_Arel_Nodes_NotEqual :not_cacheable
272
+ alias :visit_Arel_Nodes_GreaterThan :not_cacheable
273
+ alias :visit_Arel_Nodes_GreaterThanOrEqual :not_cacheable
274
+ alias :visit_Arel_Nodes_Assignment :not_cacheable
275
+ alias :visit_Arel_Nodes_LessThan :not_cacheable
276
+ alias :visit_Arel_Nodes_LessThanOrEqual :not_cacheable
277
+ alias :visit_Arel_Nodes_Between :not_cacheable
278
+ alias :visit_Arel_Nodes_NotIn :not_cacheable
279
+ alias :visit_Arel_Nodes_DoesNotMatch :not_cacheable
280
+ alias :visit_Arel_Nodes_Matches :not_cacheable
281
+
282
+ def visit_Fixnum o, attribute
283
+ o.to_i
284
+ end
285
+ alias :visit_Bignum :visit_Fixnum
286
+
287
+ def visit_Symbol o, attribute
288
+ o.to_sym
289
+ end
290
+
291
+ def visit_Object o, attribute
292
+ o
293
+ end
294
+ alias :visit_Arel_Nodes_SqlLiteral :visit_Object
295
+ alias :visit_Arel_Nodes_BindParam :visit_Object
296
+ alias :visit_Arel_SqlLiteral :visit_Object # This is deprecated
297
+ alias :visit_String :visit_Object
298
+ alias :visit_NilClass :visit_Object
299
+ alias :visit_TrueClass :visit_Object
300
+ alias :visit_FalseClass :visit_Object
301
+ alias :visit_Arel_SqlLiteral :visit_Object
302
+ alias :visit_BigDecimal :visit_Object
303
+ alias :visit_Float :visit_Object
304
+ alias :visit_Time :visit_Object
305
+ alias :visit_Date :visit_Object
306
+ alias :visit_DateTime :visit_Object
307
+ alias :visit_Hash :visit_Object
308
+
309
+ def visit_Array o, attribute
310
+ o.map{ |x| visit x }
311
+ end
312
+ end
313
+ end
314
+
315
+ end
316
+
317
+ module RecordCache
318
+
319
+ # Patch ActiveRecord::Relation to make sure update_all will invalidate all referenced records
320
+ module ActiveRecord
321
+ module UpdateAll
322
+ class << self
323
+ def included(klass)
324
+ klass.extend ClassMethods
325
+ klass.send(:include, InstanceMethods)
326
+ klass.class_eval do
327
+ alias_method_chain :update_all, :record_cache
328
+ end
329
+ end
330
+ end
331
+
332
+ module ClassMethods
333
+ end
334
+
335
+ module InstanceMethods
336
+ def __find_in_clause(sub_select)
337
+ return nil unless sub_select.arel.constraints.count == 1
338
+ constraint = sub_select.arel.constraints.first
339
+ return constraint if constraint.is_a?(::Arel::Nodes::In) # directly an IN clause
340
+ return nil unless constraint.respond_to?(:children) && constraint.children.count == 1
341
+ constraint = constraint.children.first
342
+ return constraint if constraint.is_a?(::Arel::Nodes::In) # AND with IN clause
343
+ nil
344
+ end
345
+
346
+ def update_all_with_record_cache(updates, conditions = nil, options = {})
347
+ result = update_all_without_record_cache(updates, conditions, options)
348
+
349
+ if record_cache?
350
+ # when this condition is met, the arel.update method will be called on the current scope, see ActiveRecord::Relation#update_all
351
+ unless conditions || options.present? || @limit_value.present? != @order_values.present?
352
+ # get all attributes that contain a unique index for this model
353
+ unique_index_attributes = RecordCache::Strategy::UniqueIndexCache.attributes(self)
354
+ # go straight to SQL result (without instantiating records) for optimal performance
355
+ RecordCache::Base.version_store.multi do
356
+ sub_select = select(unique_index_attributes.map(&:to_s).join(','))
357
+ in_clause = __find_in_clause(sub_select)
358
+ if unique_index_attributes.size == 1 && in_clause &&
359
+ in_clause.left.try(:name).to_s == unique_index_attributes.first.to_s
360
+ # common case where the unique index is the (only) constraint on the query: SELECT id FROM people WHERE id in (...)
361
+ attribute = unique_index_attributes.first
362
+ in_clause.right.each do |value|
363
+ record_cache.invalidate(attribute, value)
364
+ end
365
+ else
366
+ connection.execute(sub_select.to_sql).each do |row|
367
+ # invalidate the unique index for all attributes
368
+ unique_index_attributes.each_with_index do |attribute, index|
369
+ record_cache.invalidate(attribute, (row.is_a?(Hash) ? row[attribute.to_s] : row[index]))
370
+ end
371
+ end
372
+ end
373
+ end
374
+ end
375
+ end
376
+
377
+ result
378
+ end
379
+ end
380
+ end
381
+ end
382
+
383
+ # Patch ActiveRecord::Associations::HasManyAssociation to make sure the index_cache is updated when records are
384
+ # deleted from the collection
385
+ module ActiveRecord
386
+ module HasMany
387
+ class << self
388
+ def included(klass)
389
+ klass.extend ClassMethods
390
+ klass.send(:include, InstanceMethods)
391
+ klass.class_eval do
392
+ alias_method_chain :delete_records, :record_cache
393
+ end
394
+ end
395
+ end
396
+
397
+ module ClassMethods
398
+ end
399
+
400
+ module InstanceMethods
401
+ def delete_records_with_record_cache(records, method)
402
+ # invalidate :id cache for all records
403
+ records.each{ |record| record.class.record_cache.invalidate(record.id) if record.class.record_cache? unless record.new_record? }
404
+ # invalidate the referenced class for the attribute/value pair on the index cache
405
+ @reflection.klass.record_cache.invalidate(@reflection.foreign_key.to_sym, @owner.id) if @reflection.klass.record_cache?
406
+ delete_records_without_record_cache(records, method)
407
+ end
408
+ end
409
+ end
410
+
411
+ module HasOne
412
+ class << self
413
+ def included(klass)
414
+ klass.extend ClassMethods
415
+ klass.send(:include, InstanceMethods)
416
+ klass.class_eval do
417
+ alias_method_chain :delete, :record_cache
418
+ end
419
+ end
420
+ end
421
+
422
+ module ClassMethods
423
+ end
424
+
425
+ module InstanceMethods
426
+ def delete_with_record_cache(method = options[:dependent])
427
+ # invalidate :id cache for all record
428
+ if load_target
429
+ target.class.record_cache.invalidate(target.id) if target.class.record_cache? unless target.new_record?
430
+ end
431
+ # invalidate the referenced class for the attribute/value pair on the index cache
432
+ @reflection.klass.record_cache.invalidate(@reflection.foreign_key.to_sym, @owner.id) if @reflection.klass.record_cache?
433
+ delete_without_record_cache(method)
434
+ end
435
+ end
436
+ end
437
+ end
438
+
439
+ end
440
+
441
+ ActiveRecord::Base.send(:include, RecordCache::ActiveRecord::Base)
442
+ Arel::TreeManager.send(:include, RecordCache::Arel::TreeManager)
443
+ ActiveRecord::Relation.send(:include, RecordCache::ActiveRecord::UpdateAll)
444
+ ActiveRecord::Associations::HasManyAssociation.send(:include, RecordCache::ActiveRecord::HasMany)
445
+ ActiveRecord::Associations::HasOneAssociation.send(:include, RecordCache::ActiveRecord::HasOne)