identity_cache 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +3 -0
- data/README.md +3 -3
- data/identity_cache.gemspec +4 -0
- data/lib/identity_cache.rb +224 -52
- data/lib/identity_cache/version.rb +1 -1
- data/test/cache_fetch_includes_test.rb +88 -0
- data/test/denormalized_has_one_test.rb +8 -2
- data/test/fetch_multi_test.rb +19 -0
- data/test/helpers/cache.rb +2 -38
- data/test/helpers/database_connection.rb +1 -0
- data/test/recursive_denormalized_has_many_test.rb +9 -6
- data/test/schema_change_test.rb +88 -0
- data/test/test_helper.rb +49 -0
- metadata +121 -27
data/CHANGELOG
ADDED
data/README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# IdentityCache
|
2
|
-
[![Build Status](https://api.travis-ci.org/Shopify/identity_cache.png)](http://travis-ci.org/Shopify/identity_cache)
|
2
|
+
[![Build Status](https://api.travis-ci.org/Shopify/identity_cache.png?branch=master)](http://travis-ci.org/Shopify/identity_cache)
|
3
3
|
|
4
4
|
Opt in read through ActiveRecord caching used in production and extracted from Shopify. IdentityCache lets you specify how you want to cache your model objects, at the model level, and adds a number of convenience methods for accessing those objects through the cache. Memcached is used as the backend cache store, and the database is only hit when a copy of the object cannot be found in Memcached.
|
5
5
|
|
@@ -10,7 +10,7 @@ IdentityCache keeps track of the objects that have cached indexes and uses an `a
|
|
10
10
|
Add this line to your application's Gemfile:
|
11
11
|
|
12
12
|
```ruby
|
13
|
-
gem 'identity_cache'
|
13
|
+
gem 'identity_cache'
|
14
14
|
```
|
15
15
|
|
16
16
|
And then execute:
|
@@ -60,7 +60,7 @@ end
|
|
60
60
|
# If the object isn't in the cache it is pulled from the db and stored in the cache.
|
61
61
|
product = Product.fetch_by_handle(handle)
|
62
62
|
|
63
|
-
products = Product.fetch_by_vendor_and_product_type(
|
63
|
+
products = Product.fetch_by_vendor_and_product_type(vendor, product_type)
|
64
64
|
```
|
65
65
|
|
66
66
|
This gives you a lot of freedom to use your objects the way you want to, and doesn't get in your way. This does keep an independent cache copy in Memcached so you might want to watch the number of different caches that are being added.
|
data/identity_cache.gemspec
CHANGED
@@ -17,8 +17,12 @@ Gem::Specification.new do |gem|
|
|
17
17
|
|
18
18
|
|
19
19
|
gem.add_dependency('ar_transaction_changes', '0.0.1')
|
20
|
+
gem.add_dependency('activerecord', '3.2.13')
|
21
|
+
gem.add_dependency('activesupport', '3.2.13')
|
20
22
|
gem.add_dependency('cityhash', '0.6.0')
|
23
|
+
gem.add_development_dependency('memcache-client')
|
21
24
|
gem.add_development_dependency('rake')
|
22
25
|
gem.add_development_dependency('mocha')
|
23
26
|
gem.add_development_dependency('mysql2')
|
27
|
+
gem.add_development_dependency('debugger')
|
24
28
|
end
|
data/lib/identity_cache.rb
CHANGED
@@ -12,8 +12,14 @@ module IdentityCache
|
|
12
12
|
attr_accessor :logger, :readonly
|
13
13
|
attr_reader :cache
|
14
14
|
|
15
|
-
|
16
|
-
|
15
|
+
# Sets the cache adaptor IdentityCache will be using
|
16
|
+
#
|
17
|
+
# == Parameters
|
18
|
+
#
|
19
|
+
# +cache_adaptor+ - A ActiveSupport::Cache::Store
|
20
|
+
#
|
21
|
+
def cache_backend=(cache_adaptor)
|
22
|
+
cache.memcache = cache_adaptor
|
17
23
|
end
|
18
24
|
|
19
25
|
def cache
|
@@ -24,10 +30,17 @@ module IdentityCache
|
|
24
30
|
@logger || Rails.logger
|
25
31
|
end
|
26
32
|
|
27
|
-
def should_cache?
|
33
|
+
def should_cache? # :nodoc:
|
28
34
|
!readonly && ActiveRecord::Base.connection.open_transactions == 0
|
29
35
|
end
|
30
36
|
|
37
|
+
# Cache retrieval and miss resolver primitive; given a key it will try to
|
38
|
+
# retrieve the associated value from the cache otherwise it will return the
|
39
|
+
# value of the execution of the block.
|
40
|
+
#
|
41
|
+
# == Parameters
|
42
|
+
# +key+ A cache key string
|
43
|
+
#
|
31
44
|
def fetch(key, &block)
|
32
45
|
result = cache.read(key) if should_cache?
|
33
46
|
|
@@ -37,6 +50,7 @@ module IdentityCache
|
|
37
50
|
result = yield
|
38
51
|
end
|
39
52
|
result = map_cached_nil_for(result)
|
53
|
+
|
40
54
|
if should_cache?
|
41
55
|
cache.write(key, result)
|
42
56
|
end
|
@@ -58,6 +72,11 @@ module IdentityCache
|
|
58
72
|
value == IdentityCache::CACHED_NIL ? nil : value
|
59
73
|
end
|
60
74
|
|
75
|
+
# Same as +fetch+, except that it will try a collection of keys, using the
|
76
|
+
# multiget operation of the cache adaptor
|
77
|
+
#
|
78
|
+
# == Parameters
|
79
|
+
# +keys+ A collection of key strings
|
61
80
|
def fetch_multi(*keys, &block)
|
62
81
|
return {} if keys.size == 0
|
63
82
|
result = {}
|
@@ -93,7 +112,11 @@ module IdentityCache
|
|
93
112
|
result
|
94
113
|
end
|
95
114
|
|
96
|
-
def
|
115
|
+
def schema_to_string(columns)
|
116
|
+
columns.sort_by(&:name).map {|c| "#{c.name}:#{c.type}"} * ","
|
117
|
+
end
|
118
|
+
|
119
|
+
def included(base) #:nodoc:
|
97
120
|
raise AlreadyIncludedError if base.respond_to? :cache_indexes
|
98
121
|
|
99
122
|
unless ActiveRecord::Base.connection.respond_to?(:with_master)
|
@@ -112,26 +135,55 @@ module IdentityCache
|
|
112
135
|
base.class_attribute :cache_attributes
|
113
136
|
base.class_attribute :cached_has_manys
|
114
137
|
base.class_attribute :cached_has_ones
|
138
|
+
base.class_attribute :embedded_schema_hashes
|
115
139
|
base.send(:extend, ClassMethods)
|
116
140
|
|
141
|
+
base.cached_has_manys = {}
|
142
|
+
base.cached_has_ones = {}
|
143
|
+
base.embedded_schema_hashes = {}
|
144
|
+
base.cache_attributes = []
|
145
|
+
base.cache_indexes = []
|
146
|
+
|
117
147
|
base.private_class_method :require_if_necessary, :build_normalized_has_many_cache, :build_denormalized_association_cache, :add_parent_expiry_hook,
|
118
148
|
:identity_cache_multiple_value_dynamic_fetcher, :identity_cache_single_value_dynamic_fetcher
|
119
149
|
|
150
|
+
|
120
151
|
base.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
|
121
152
|
private :expire_cache, :was_new_record?, :fetch_denormalized_cached_association, :populate_denormalized_cached_association
|
122
153
|
CODE
|
123
154
|
end
|
124
155
|
|
125
|
-
def memcache_hash(key)
|
156
|
+
def memcache_hash(key) #:nodoc:
|
126
157
|
CityHash.hash64(key)
|
127
158
|
end
|
128
159
|
end
|
129
160
|
|
130
161
|
module ClassMethods
|
131
162
|
|
163
|
+
# Declares a new index in the cache for the class where IdentityCache was
|
164
|
+
# included.
|
165
|
+
#
|
166
|
+
# IdentityCache will add a fetch_by_field1_and_field2_and_...field for every
|
167
|
+
# index.
|
168
|
+
#
|
169
|
+
# == Example:
|
170
|
+
#
|
171
|
+
# class Product
|
172
|
+
# include IdentityCache
|
173
|
+
# cache_index :name, :vendor
|
174
|
+
# end
|
175
|
+
#
|
176
|
+
# Will add Product.fetch_by_name_and_vendor
|
177
|
+
#
|
178
|
+
# == Parameters
|
179
|
+
#
|
180
|
+
# +fields+ Array of symbols or strings representing the fields in the index
|
181
|
+
#
|
182
|
+
# == Options
|
183
|
+
# * unique: if the index would only have unique values
|
184
|
+
#
|
132
185
|
def cache_index(*fields)
|
133
186
|
options = fields.extract_options!
|
134
|
-
self.cache_indexes ||= []
|
135
187
|
self.cache_indexes.push fields
|
136
188
|
|
137
189
|
field_list = fields.join("_and_")
|
@@ -160,7 +212,7 @@ module IdentityCache
|
|
160
212
|
end
|
161
213
|
end
|
162
214
|
|
163
|
-
def identity_cache_single_value_dynamic_fetcher(fields, values, sql_on_miss)
|
215
|
+
def identity_cache_single_value_dynamic_fetcher(fields, values, sql_on_miss) # :nodoc:
|
164
216
|
cache_key = rails_cache_index_key_for_fields_and_values(fields, values)
|
165
217
|
id = IdentityCache.fetch(cache_key) { connection.select_value(sql_on_miss) }
|
166
218
|
unless id.nil?
|
@@ -171,18 +223,42 @@ module IdentityCache
|
|
171
223
|
record
|
172
224
|
end
|
173
225
|
|
174
|
-
def identity_cache_multiple_value_dynamic_fetcher(fields, values, sql_on_miss)
|
226
|
+
def identity_cache_multiple_value_dynamic_fetcher(fields, values, sql_on_miss) # :nodoc:
|
175
227
|
cache_key = rails_cache_index_key_for_fields_and_values(fields, values)
|
176
228
|
ids = IdentityCache.fetch(cache_key) { connection.select_values(sql_on_miss) }
|
177
229
|
|
178
230
|
ids.empty? ? [] : fetch_multi(*ids)
|
179
231
|
end
|
180
232
|
|
233
|
+
|
234
|
+
# Will cache an association to the class including IdentityCache.
|
235
|
+
# The embed option, if set, will make IdentityCache keep the association
|
236
|
+
# values in the same cache entry as the parent.
|
237
|
+
#
|
238
|
+
# Embedded associations are more effective in offloading database work,
|
239
|
+
# however they will increase the size of the cache entries and make the
|
240
|
+
# whole entry expire when any of the embedded members change.
|
241
|
+
#
|
242
|
+
# == Example:
|
243
|
+
# class Product
|
244
|
+
# cached_has_many :options, :embed => false
|
245
|
+
# cached_has_many :orders
|
246
|
+
# cached_has_many :buyers, :inverse_name => 'line_item'
|
247
|
+
# end
|
248
|
+
#
|
249
|
+
# == Parameters
|
250
|
+
# +association+ Name of the association being cached as a symbol
|
251
|
+
#
|
252
|
+
# == Options
|
253
|
+
#
|
254
|
+
# * embed: If set will cause IdentityCache to keep the values for this
|
255
|
+
# association in the same cache entry as the parent, instead of its own.
|
256
|
+
# * inverse_name: The name of the parent in the association if the name is
|
257
|
+
# not the lowercase pluralization of the parent object's class
|
181
258
|
def cache_has_many(association, options = {})
|
182
259
|
options[:embed] ||= false
|
183
260
|
options[:inverse_name] ||= self.name.underscore.to_sym
|
184
261
|
raise InverseAssociationError unless self.reflect_on_association(association)
|
185
|
-
self.cached_has_manys ||= {}
|
186
262
|
self.cached_has_manys[association] = options
|
187
263
|
|
188
264
|
if options[:embed]
|
@@ -192,21 +268,45 @@ module IdentityCache
|
|
192
268
|
end
|
193
269
|
end
|
194
270
|
|
271
|
+
# Will cache an association to the class including IdentityCache.
|
272
|
+
# The embed option if set will make IdentityCache keep the association
|
273
|
+
# values in the same cache entry as the parent.
|
274
|
+
#
|
275
|
+
# Embedded associations are more effective in offloading database work,
|
276
|
+
# however they will increase the size of the cache entries and make the
|
277
|
+
# whole entry expire with the change of any of the embedded members
|
278
|
+
#
|
279
|
+
# == Example:
|
280
|
+
# class Product
|
281
|
+
# cached_has_one :store, :embed => false
|
282
|
+
# cached_has_one :vendor
|
283
|
+
# end
|
284
|
+
#
|
285
|
+
# == Parameters
|
286
|
+
# +association+ Symbol with the name of the association being cached
|
287
|
+
#
|
288
|
+
# == Options
|
289
|
+
#
|
290
|
+
# * embed: If set will cause IdentityCache to keep the values for this
|
291
|
+
# association in the same cache entry as the parent, instead of its own.
|
292
|
+
# * inverse_name: The name of the parent in the association ( only
|
293
|
+
# necessary if the name is not the lowercase pluralization of the
|
294
|
+
# parent object's class)
|
195
295
|
def cache_has_one(association, options = {})
|
196
296
|
options[:embed] ||= true
|
197
297
|
options[:inverse_name] ||= self.name.underscore.to_sym
|
198
298
|
raise InverseAssociationError unless self.reflect_on_association(association)
|
199
|
-
self.cached_has_ones ||= {}
|
200
299
|
self.cached_has_ones[association] = options
|
201
300
|
|
202
301
|
build_denormalized_association_cache(association, options)
|
203
302
|
end
|
204
303
|
|
205
|
-
def build_denormalized_association_cache(association, options)
|
304
|
+
def build_denormalized_association_cache(association, options) #:nodoc:
|
206
305
|
options[:cached_accessor_name] ||= "fetch_#{association}"
|
207
306
|
options[:cache_variable_name] ||= "cached_#{association}"
|
208
307
|
options[:population_method_name] ||= "populate_#{association}_cache"
|
209
308
|
|
309
|
+
|
210
310
|
unless instance_methods.include?(options[:cached_accessor_name].to_sym)
|
211
311
|
self.class_eval(ruby = <<-CODE, __FILE__, __LINE__)
|
212
312
|
def #{options[:cached_accessor_name]}
|
@@ -223,7 +323,7 @@ module IdentityCache
|
|
223
323
|
end
|
224
324
|
end
|
225
325
|
|
226
|
-
def build_normalized_has_many_cache(association, options)
|
326
|
+
def build_normalized_has_many_cache(association, options) #:nodoc:
|
227
327
|
singular_association = association.to_s.singularize
|
228
328
|
association_class = reflect_on_association(association).klass
|
229
329
|
options[:cached_accessor_name] ||= "fetch_#{association}"
|
@@ -251,11 +351,26 @@ module IdentityCache
|
|
251
351
|
add_parent_expiry_hook(association_class, options.merge(:only_on_foreign_key_change => true))
|
252
352
|
end
|
253
353
|
|
354
|
+
|
355
|
+
# Will cache a single attribute on its own blob, it will add a
|
356
|
+
# fetch_attribute_by_id (or the value of the by option).
|
357
|
+
#
|
358
|
+
# == Example:
|
359
|
+
# class Product
|
360
|
+
# cache_attribute :quantity, :by => :name
|
361
|
+
# cache_attribute :quantity :by => [:name, :vendor]
|
362
|
+
# end
|
363
|
+
#
|
364
|
+
# == Parameters
|
365
|
+
# +attribute+ Symbol with the name of the attribute being cached
|
366
|
+
#
|
367
|
+
# == Options
|
368
|
+
#
|
369
|
+
# * by: Other attribute or attributes in the model to keep values indexed. Default is :id
|
254
370
|
def cache_attribute(attribute, options = {})
|
255
371
|
options[:by] ||= :id
|
256
372
|
fields = Array(options[:by])
|
257
373
|
|
258
|
-
self.cache_attributes ||= []
|
259
374
|
self.cache_attributes.push [attribute, fields]
|
260
375
|
|
261
376
|
field_list = fields.join("_and_")
|
@@ -270,21 +385,24 @@ module IdentityCache
|
|
270
385
|
CODE
|
271
386
|
end
|
272
387
|
|
273
|
-
def attribute_dynamic_fetcher(attribute, fields, values, sql_on_miss)
|
388
|
+
def attribute_dynamic_fetcher(attribute, fields, values, sql_on_miss) #:nodoc:
|
274
389
|
cache_key = rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values)
|
275
390
|
IdentityCache.fetch(cache_key) { connection.select_value(sql_on_miss) }
|
276
391
|
end
|
277
392
|
|
393
|
+
# Similar to ActiveRecord::Base#exists? will return true if the id can be
|
394
|
+
# found in the cache.
|
278
395
|
def exists_with_identity_cache?(id)
|
279
396
|
!!fetch_by_id(id)
|
280
397
|
end
|
281
398
|
|
399
|
+
# Default fetcher added to the model on inclusion, it behaves like
|
400
|
+
# ActiveRecord::Base.find_by_id
|
282
401
|
def fetch_by_id(id)
|
283
402
|
if IdentityCache.should_cache?
|
284
403
|
|
285
404
|
require_if_necessary do
|
286
405
|
object = IdentityCache.fetch(rails_cache_key(id)){ resolve_cache_miss(id) }
|
287
|
-
object.clear_association_cache if object.respond_to?(:clear_association_cache)
|
288
406
|
IdentityCache.logger.error "[IDC id mismatch] fetch_by_id_requested=#{id} fetch_by_id_got=#{object.id} for #{object.inspect[(0..100)]} " if object && object.id != id.to_i
|
289
407
|
object
|
290
408
|
end
|
@@ -294,11 +412,18 @@ module IdentityCache
|
|
294
412
|
end
|
295
413
|
end
|
296
414
|
|
415
|
+
# Default fetcher added to the model on inclusion, it behaves like
|
416
|
+
# ActiveRecord::Base.find, will raise ActiveRecord::RecordNotFound exception
|
417
|
+
# if id is not in the cache or the db.
|
297
418
|
def fetch(id)
|
298
419
|
fetch_by_id(id) or raise(ActiveRecord::RecordNotFound, "Couldn't find #{self.class.name} with ID=#{id}")
|
299
420
|
end
|
300
421
|
|
422
|
+
|
423
|
+
# Default fetcher added to the model on inclusion, if behaves like
|
424
|
+
# ActiveRecord::Base.find_all_by_id
|
301
425
|
def fetch_multi(*ids)
|
426
|
+
options = ids.extract_options!
|
302
427
|
if IdentityCache.should_cache?
|
303
428
|
|
304
429
|
require_if_necessary do
|
@@ -307,25 +432,20 @@ module IdentityCache
|
|
307
432
|
|
308
433
|
objects_by_key = IdentityCache.fetch_multi(*key_to_id_map.keys) do |unresolved_keys|
|
309
434
|
ids = unresolved_keys.map {|key| key_to_id_map[key] }
|
310
|
-
records = find_batch(ids)
|
435
|
+
records = find_batch(ids, options)
|
311
436
|
records.compact.each(&:populate_association_caches)
|
312
437
|
records
|
313
438
|
end
|
314
439
|
|
315
|
-
|
316
|
-
objects_in_order.each do |object|
|
317
|
-
object.clear_association_cache if object.respond_to?(:clear_association_cache)
|
318
|
-
end
|
319
|
-
|
320
|
-
objects_in_order.compact
|
440
|
+
cache_keys.map {|key| objects_by_key[key] }.compact
|
321
441
|
end
|
322
442
|
|
323
443
|
else
|
324
|
-
find_batch(ids)
|
444
|
+
find_batch(ids, options)
|
325
445
|
end
|
326
446
|
end
|
327
447
|
|
328
|
-
def require_if_necessary
|
448
|
+
def require_if_necessary #:nodoc:
|
329
449
|
# mem_cache_store returns raw value if unmarshal fails
|
330
450
|
rval = yield
|
331
451
|
case rval
|
@@ -343,7 +463,7 @@ module IdentityCache
|
|
343
463
|
raise
|
344
464
|
end
|
345
465
|
|
346
|
-
module ParentModelExpiration
|
466
|
+
module ParentModelExpiration # :nodoc:
|
347
467
|
def expire_parent_cache_on_changes(parent_name, foreign_key, parent_class, options = {})
|
348
468
|
new_parent = send(parent_name)
|
349
469
|
|
@@ -403,25 +523,43 @@ module IdentityCache
|
|
403
523
|
end
|
404
524
|
|
405
525
|
def all_cached_associations
|
406
|
-
(cached_has_manys || {}).merge(cached_has_ones || {})
|
526
|
+
(cached_has_manys || {}).merge(cached_has_ones || {}).merge(cached_belongs_tos || {})
|
527
|
+
end
|
528
|
+
|
529
|
+
def all_cached_associations_needing_population
|
530
|
+
all_cached_associations.select do |cached_association, options|
|
531
|
+
options[:population_method_name].present? # non-embedded belongs_to associations don't need population
|
532
|
+
end
|
407
533
|
end
|
408
534
|
|
409
|
-
def cache_fetch_includes
|
410
|
-
|
535
|
+
def cache_fetch_includes(additions = {})
|
536
|
+
additions = hashify_includes_structure(additions)
|
537
|
+
embedded_associations = all_cached_associations.select { |name, options| options[:embed] }
|
538
|
+
|
539
|
+
associations_for_identity_cache = embedded_associations.map do |child_association, options|
|
411
540
|
child_class = reflect_on_association(child_association).try(:klass)
|
412
|
-
|
413
|
-
|
541
|
+
|
542
|
+
child_includes = additions.delete(child_association)
|
543
|
+
|
544
|
+
if child_class.respond_to?(:cache_fetch_includes)
|
545
|
+
child_includes = child_class.cache_fetch_includes(child_includes)
|
546
|
+
end
|
547
|
+
|
548
|
+
if child_includes.blank?
|
414
549
|
child_association
|
415
550
|
else
|
416
|
-
{ child_association =>
|
551
|
+
{ child_association => child_includes }
|
417
552
|
end
|
418
553
|
end
|
554
|
+
|
555
|
+
associations_for_identity_cache.push(additions) if additions.keys.size > 0
|
556
|
+
associations_for_identity_cache.compact
|
419
557
|
end
|
420
558
|
|
421
|
-
def find_batch(ids)
|
559
|
+
def find_batch(ids, options = {})
|
422
560
|
@id_column ||= columns.detect {|c| c.name == "id"}
|
423
561
|
ids = ids.map{ |id| @id_column.type_cast(id) }
|
424
|
-
records = where('id IN (?)', ids).includes(cache_fetch_includes).all
|
562
|
+
records = where('id IN (?)', ids).includes(cache_fetch_includes(options[:includes])).all
|
425
563
|
records_by_id = records.index_by(&:id)
|
426
564
|
records = ids.map{ |id| records_by_id[id] }
|
427
565
|
mismatching_ids = records.compact.map(&:id) - ids
|
@@ -433,10 +571,10 @@ module IdentityCache
|
|
433
571
|
rails_cache_key_prefix + id.to_s
|
434
572
|
end
|
435
573
|
|
574
|
+
|
436
575
|
def rails_cache_key_prefix
|
437
576
|
@rails_cache_key_prefix ||= begin
|
438
|
-
|
439
|
-
"IDC:blob:#{base_class.name}:#{IdentityCache.memcache_hash(column_list)}:"
|
577
|
+
"IDC:blob:#{base_class.name}:#{IdentityCache.memcache_hash(IdentityCache.schema_to_string(columns))}:"
|
440
578
|
end
|
441
579
|
end
|
442
580
|
|
@@ -451,10 +589,32 @@ module IdentityCache
|
|
451
589
|
def rails_cache_string_for_fields_and_values(fields, values)
|
452
590
|
"#{fields.join('/')}:#{IdentityCache.memcache_hash(values.join('/'))}"
|
453
591
|
end
|
592
|
+
|
593
|
+
private
|
594
|
+
|
595
|
+
def hashify_includes_structure(structure)
|
596
|
+
case structure
|
597
|
+
when nil
|
598
|
+
{}
|
599
|
+
when Symbol
|
600
|
+
{structure => []}
|
601
|
+
when Hash
|
602
|
+
structure.clone
|
603
|
+
when Array
|
604
|
+
structure.each_with_object({}) do |member, hash|
|
605
|
+
case member
|
606
|
+
when Hash
|
607
|
+
hash.merge(hash)
|
608
|
+
when Symbol
|
609
|
+
hash[member] = []
|
610
|
+
end
|
611
|
+
end
|
612
|
+
end
|
613
|
+
end
|
454
614
|
end
|
455
615
|
|
456
|
-
def populate_association_caches
|
457
|
-
self.class.
|
616
|
+
def populate_association_caches # :nodoc:
|
617
|
+
self.class.all_cached_associations_needing_population.each do |cached_association, options|
|
458
618
|
send(options[:population_method_name])
|
459
619
|
reflection = options[:embed] && self.class.reflect_on_association(cached_association)
|
460
620
|
if reflection && reflection.klass.respond_to?(:cached_has_manys)
|
@@ -462,9 +622,10 @@ module IdentityCache
|
|
462
622
|
child_objects.each(&:populate_association_caches)
|
463
623
|
end
|
464
624
|
end
|
625
|
+
self.clear_association_cache if self.respond_to?(:clear_association_cache)
|
465
626
|
end
|
466
627
|
|
467
|
-
def fetch_denormalized_cached_association(ivar_name, association_name)
|
628
|
+
def fetch_denormalized_cached_association(ivar_name, association_name) # :nodoc:
|
468
629
|
ivar_full_name = :"@#{ivar_name}"
|
469
630
|
if IdentityCache.should_cache?
|
470
631
|
populate_denormalized_cached_association(ivar_name, association_name)
|
@@ -474,36 +635,47 @@ module IdentityCache
|
|
474
635
|
end
|
475
636
|
end
|
476
637
|
|
477
|
-
def populate_denormalized_cached_association(ivar_name, association_name)
|
638
|
+
def populate_denormalized_cached_association(ivar_name, association_name) # :nodoc:
|
478
639
|
ivar_full_name = :"@#{ivar_name}"
|
640
|
+
schema_hash_ivar = :"@#{ivar_name}_schema_hash"
|
641
|
+
reflection = association(association_name)
|
479
642
|
|
480
|
-
|
481
|
-
|
643
|
+
current_schema_hash = self.class.embedded_schema_hashes[association_name] ||= begin
|
644
|
+
IdentityCache.memcache_hash(IdentityCache.schema_to_string(reflection.klass.columns))
|
645
|
+
end
|
646
|
+
|
647
|
+
saved_schema_hash = instance_variable_get(schema_hash_ivar)
|
648
|
+
|
649
|
+
if saved_schema_hash == current_schema_hash
|
650
|
+
value = instance_variable_get(ivar_full_name)
|
651
|
+
return value unless value.nil?
|
652
|
+
end
|
482
653
|
|
483
|
-
reflection = association(association_name)
|
484
654
|
reflection.load_target unless reflection.loaded?
|
485
655
|
|
486
656
|
loaded_association = send(association_name)
|
657
|
+
|
658
|
+
instance_variable_set(schema_hash_ivar, current_schema_hash)
|
487
659
|
instance_variable_set(ivar_full_name, IdentityCache.map_cached_nil_for(loaded_association))
|
488
660
|
end
|
489
661
|
|
490
|
-
def primary_cache_index_key
|
662
|
+
def primary_cache_index_key # :nodoc:
|
491
663
|
self.class.rails_cache_key(id)
|
492
664
|
end
|
493
665
|
|
494
|
-
def secondary_cache_index_key_for_current_values(fields)
|
666
|
+
def secondary_cache_index_key_for_current_values(fields) # :nodoc:
|
495
667
|
self.class.rails_cache_index_key_for_fields_and_values(fields, fields.collect {|field| self.send(field)})
|
496
668
|
end
|
497
669
|
|
498
|
-
def secondary_cache_index_key_for_previous_values(fields)
|
670
|
+
def secondary_cache_index_key_for_previous_values(fields) # :nodoc:
|
499
671
|
self.class.rails_cache_index_key_for_fields_and_values(fields, old_values_for_fields(fields))
|
500
672
|
end
|
501
673
|
|
502
|
-
def attribute_cache_key_for_attribute_and_previous_values(attribute, fields)
|
674
|
+
def attribute_cache_key_for_attribute_and_previous_values(attribute, fields) # :nodoc:
|
503
675
|
self.class.rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, old_values_for_fields(fields))
|
504
676
|
end
|
505
677
|
|
506
|
-
def old_values_for_fields(fields)
|
678
|
+
def old_values_for_fields(fields) # :nodoc:
|
507
679
|
fields.map do |field|
|
508
680
|
field_string = field.to_s
|
509
681
|
if destroyed? && transaction_changed_attributes.has_key?(field_string)
|
@@ -516,7 +688,7 @@ module IdentityCache
|
|
516
688
|
end
|
517
689
|
end
|
518
690
|
|
519
|
-
def expire_primary_index
|
691
|
+
def expire_primary_index # :nodoc:
|
520
692
|
extra_keys = if respond_to? :updated_at
|
521
693
|
old_updated_at = old_values_for_fields([:updated_at]).first
|
522
694
|
"expiring_last_updated_at=#{old_updated_at}"
|
@@ -528,7 +700,7 @@ module IdentityCache
|
|
528
700
|
IdentityCache.cache.delete(primary_cache_index_key)
|
529
701
|
end
|
530
702
|
|
531
|
-
def expire_secondary_indexes
|
703
|
+
def expire_secondary_indexes # :nodoc:
|
532
704
|
cache_indexes.try(:each) do |fields|
|
533
705
|
if self.destroyed?
|
534
706
|
IdentityCache.cache.delete(secondary_cache_index_key_for_previous_values(fields))
|
@@ -544,20 +716,20 @@ module IdentityCache
|
|
544
716
|
end
|
545
717
|
end
|
546
718
|
|
547
|
-
def expire_attribute_indexes
|
719
|
+
def expire_attribute_indexes # :nodoc:
|
548
720
|
cache_attributes.try(:each) do |(attribute, fields)|
|
549
721
|
IdentityCache.cache.delete(attribute_cache_key_for_attribute_and_previous_values(attribute, fields)) unless was_new_record?
|
550
722
|
end
|
551
723
|
end
|
552
724
|
|
553
|
-
def expire_cache
|
725
|
+
def expire_cache # :nodoc:
|
554
726
|
expire_primary_index
|
555
727
|
expire_secondary_indexes
|
556
728
|
expire_attribute_indexes
|
557
729
|
true
|
558
730
|
end
|
559
731
|
|
560
|
-
def was_new_record?
|
732
|
+
def was_new_record? # :nodoc:
|
561
733
|
!destroyed? && transaction_changed_attributes.has_key?('id') && transaction_changed_attributes['id'].nil?
|
562
734
|
end
|
563
735
|
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class CacheFetchIncludesTest < IdentityCache::TestCase
|
4
|
+
def setup
|
5
|
+
super
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_cached_embedded_has_manys_are_included_in_includes
|
9
|
+
Record.send(:cache_has_many, :associated_records, :embed => true)
|
10
|
+
assert_equal [:associated_records], Record.cache_fetch_includes
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_cached_nonembedded_has_manys_are_included_in_includes
|
14
|
+
Record.send(:cache_has_many, :associated_records, :embed => false)
|
15
|
+
assert_equal [], Record.cache_fetch_includes
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_cached_has_ones_are_included_in_includes
|
19
|
+
Record.send(:cache_has_one, :associated)
|
20
|
+
assert_equal [:associated], Record.cache_fetch_includes
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_cached_nonembedded_belongs_tos_are_not_included_in_includes
|
24
|
+
Record.send(:cache_belongs_to, :record)
|
25
|
+
assert_equal [], Record.cache_fetch_includes
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_cached_child_associations_are_included_in_includes
|
29
|
+
Record.send(:cache_has_many, :associated_records, :embed => true)
|
30
|
+
AssociatedRecord.send(:cache_has_many, :deeply_associated_records, :embed => true)
|
31
|
+
assert_equal [{:associated_records => [:deeply_associated_records]}], Record.cache_fetch_includes
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_multiple_cached_associations_and_child_associations_are_included_in_includes
|
35
|
+
Record.send(:cache_has_many, :associated_records, :embed => true)
|
36
|
+
Record.send(:cache_has_many, :polymorphic_records, {:inverse_name => :owner, :embed => true})
|
37
|
+
Record.send(:cache_has_one, :associated, :embed => true)
|
38
|
+
AssociatedRecord.send(:cache_has_many, :deeply_associated_records, :embed => true)
|
39
|
+
assert_equal [
|
40
|
+
{:associated_records => [:deeply_associated_records]},
|
41
|
+
:polymorphic_records,
|
42
|
+
{:associated => [:deeply_associated_records]}
|
43
|
+
], Record.cache_fetch_includes
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_empty_additions_for_top_level_associations_makes_no_difference
|
47
|
+
Record.send(:cache_has_many, :associated_records, :embed => true)
|
48
|
+
assert_equal [:associated_records], Record.cache_fetch_includes({})
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_top_level_additions_are_included_in_includes
|
52
|
+
assert_equal [{:associated_records => []}], Record.cache_fetch_includes({:associated_records => []})
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_top_level_additions_alongside_top_level_cached_associations_are_included_in_includes
|
56
|
+
Record.send(:cache_has_many, :associated_records, :embed => true)
|
57
|
+
assert_equal [
|
58
|
+
:associated_records,
|
59
|
+
{:polymorphic_records => []}
|
60
|
+
], Record.cache_fetch_includes({:polymorphic_records => []})
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_child_level_additions_for_top_level_cached_associations_are_included_in_includes
|
64
|
+
Record.send(:cache_has_many, :associated_records, :embed => true)
|
65
|
+
assert_equal [
|
66
|
+
{:associated_records => [{:deeply_associated_records => []}]}
|
67
|
+
], Record.cache_fetch_includes({:associated_records => :deeply_associated_records})
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_array_child_level_additions_for_top_level_cached_associations_are_included_in_includes
|
71
|
+
Record.send(:cache_has_many, :associated_records, :embed => true)
|
72
|
+
assert_equal [
|
73
|
+
{:associated_records => [{:deeply_associated_records => []}]}
|
74
|
+
], Record.cache_fetch_includes({:associated_records => [:deeply_associated_records]})
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_array_child_level_additions_for_child_level_cached_associations_are_included_in_includes
|
78
|
+
Record.send(:cache_has_many, :associated_records, :embed => true)
|
79
|
+
AssociatedRecord.send(:cache_has_many, :deeply_associated_records, :embed => true)
|
80
|
+
assert_equal [
|
81
|
+
{:associated_records => [
|
82
|
+
:deeply_associated_records,
|
83
|
+
{:record => []}
|
84
|
+
]}
|
85
|
+
], Record.cache_fetch_includes({:associated_records => [:record]})
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -24,7 +24,10 @@ class DenormalizedHasOneTest < IdentityCache::TestCase
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def test_on_cache_miss_record_should_embed_nil_object
|
27
|
-
|
27
|
+
|
28
|
+
@record.associated = nil
|
29
|
+
@record.save!
|
30
|
+
@record.reload
|
28
31
|
Record.expects(:find_by_id).with(@record.id, :include => Record.cache_fetch_includes).returns(@record)
|
29
32
|
IdentityCache.cache.expects(:read).with(@record.secondary_cache_index_key_for_current_values([:title]))
|
30
33
|
IdentityCache.cache.expects(:read).with(@record.primary_cache_index_key)
|
@@ -57,7 +60,10 @@ class DenormalizedHasOneTest < IdentityCache::TestCase
|
|
57
60
|
end
|
58
61
|
|
59
62
|
def test_on_cache_hit_record_should_come_back_with_cached_nil_association
|
60
|
-
@record.
|
63
|
+
@record.associated = nil
|
64
|
+
@record.save!
|
65
|
+
@record.reload
|
66
|
+
|
61
67
|
Record.expects(:find_by_id).with(1, :include => Record.cache_fetch_includes).once.returns(@record)
|
62
68
|
Record.fetch_by_title('foo')
|
63
69
|
|
data/test/fetch_multi_test.rb
CHANGED
@@ -110,6 +110,7 @@ class FetchMultiTest < IdentityCache::TestCase
|
|
110
110
|
def test_fetch_multi_includes_cached_associations
|
111
111
|
Record.send(:cache_has_many, :associated_records, :embed => true)
|
112
112
|
Record.send(:cache_has_one, :associated)
|
113
|
+
Record.send(:cache_belongs_to, :record)
|
113
114
|
|
114
115
|
cache_response = {}
|
115
116
|
cache_response[@bob_blob_key] = nil
|
@@ -124,6 +125,24 @@ class FetchMultiTest < IdentityCache::TestCase
|
|
124
125
|
assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id)
|
125
126
|
end
|
126
127
|
|
128
|
+
def test_fetch_multi_includes_cached_associations_and_other_asked_for_associations
|
129
|
+
Record.send(:cache_has_many, :associated_records, :embed => true)
|
130
|
+
Record.send(:cache_has_one, :associated)
|
131
|
+
Record.send(:cache_belongs_to, :record)
|
132
|
+
|
133
|
+
cache_response = {}
|
134
|
+
cache_response[@bob_blob_key] = nil
|
135
|
+
cache_response[@joe_blob_key] = nil
|
136
|
+
cache_response[@fred_blob_key] = nil
|
137
|
+
|
138
|
+
IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @joe_blob_key, @fred_blob_key).returns(cache_response)
|
139
|
+
|
140
|
+
mock_relation = mock("ActiveRecord::Relation")
|
141
|
+
Record.expects(:where).returns(mock_relation)
|
142
|
+
mock_relation.expects(:includes).with([:associated_records, :associated, {:record => []}]).returns(stub(:all => [@bob, @joe, @fred]))
|
143
|
+
assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id, {:includes => :record})
|
144
|
+
end
|
145
|
+
|
127
146
|
def test_find_batch_coerces_ids_to_primary_key_type
|
128
147
|
mock_relation = mock("ActiveRecord::Relation")
|
129
148
|
Record.expects(:where).returns(mock_relation)
|
data/test/helpers/cache.rb
CHANGED
@@ -1,46 +1,10 @@
|
|
1
1
|
module Rails
|
2
|
-
class Cache
|
3
2
|
|
4
|
-
|
5
|
-
@cache = {}
|
6
|
-
end
|
7
|
-
|
8
|
-
def fetch(e)
|
9
|
-
if @cache.key?(e)
|
10
|
-
return read(e)
|
11
|
-
else
|
12
|
-
a = yield
|
13
|
-
write(e,a)
|
14
|
-
return a
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def clear
|
19
|
-
@cache.clear
|
20
|
-
end
|
21
|
-
|
22
|
-
def write(a,b)
|
23
|
-
@cache[a] = b
|
24
|
-
end
|
25
|
-
|
26
|
-
def delete(a)
|
27
|
-
@cache.delete(a)
|
28
|
-
end
|
29
|
-
|
30
|
-
def read(a)
|
31
|
-
@cache[a]
|
32
|
-
end
|
33
|
-
|
34
|
-
def read_multi(*keys)
|
35
|
-
keys.reduce({}) do |hash, key|
|
36
|
-
hash[key] = @cache[key]
|
37
|
-
hash
|
38
|
-
end
|
39
|
-
end
|
3
|
+
class Cache < ActiveSupport::Cache::MemCacheStore
|
40
4
|
end
|
41
5
|
|
42
6
|
def self.cache
|
43
|
-
@@cache ||= Cache.new
|
7
|
+
@@cache ||= Cache.new("localhost:#{$memcached_port}")
|
44
8
|
end
|
45
9
|
|
46
10
|
class Logger
|
@@ -55,13 +55,16 @@ class RecursiveDenormalizedHasManyTest < IdentityCache::TestCase
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def test_on_cache_miss_child_record_fetch_should_include_nested_associations_to_avoid_n_plus_ones
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
58
|
+
assert_queries(5) do
|
59
|
+
# one for the top level record
|
60
|
+
# one for the mid level has_many association
|
61
|
+
# one for the mid level has_one association
|
62
|
+
# one for the deep level level has_many on the mid level has_many association
|
63
|
+
# one for the deep level level has_many on the mid level has_one association
|
64
|
+
record_from_cache_miss = Record.fetch(@record.id)
|
65
|
+
end
|
63
66
|
end
|
64
|
-
|
67
|
+
|
65
68
|
def test_saving_child_record_should_expire_parent_record
|
66
69
|
IdentityCache.cache.expects(:delete).with(@record.primary_cache_index_key)
|
67
70
|
IdentityCache.cache.expects(:delete).with(@associated_record.primary_cache_index_key)
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class SchemaChangeTest < IdentityCache::TestCase
|
4
|
+
class AddColumnToChild < ActiveRecord::Migration
|
5
|
+
def up
|
6
|
+
add_column :associated_records, :shiny, :string
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class AddColumnToDeepChild < ActiveRecord::Migration
|
11
|
+
def up
|
12
|
+
add_column :deeply_associated_records, :new_column, :string
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def setup
|
17
|
+
super
|
18
|
+
ActiveRecord::Migration.verbose = false
|
19
|
+
|
20
|
+
read_new_schema
|
21
|
+
Record.cache_has_one :associated, :embed => true
|
22
|
+
AssociatedRecord.cache_has_many :deeply_associated_records, :embed => true
|
23
|
+
|
24
|
+
@associated_record = AssociatedRecord.new(:name => 'bar')
|
25
|
+
@deeply_associated_record = DeeplyAssociatedRecord.new(:name => "corge")
|
26
|
+
@associated_record.deeply_associated_records << @deeply_associated_record
|
27
|
+
@associated_record.deeply_associated_records << DeeplyAssociatedRecord.new(:name => "qux")
|
28
|
+
@record = Record.new(:title => 'foo')
|
29
|
+
@record.associated = @associated_record
|
30
|
+
|
31
|
+
@associated_record.save!
|
32
|
+
@record.save!
|
33
|
+
|
34
|
+
@record.reload
|
35
|
+
end
|
36
|
+
|
37
|
+
# This helper simulates the models being reloaded
|
38
|
+
def read_new_schema
|
39
|
+
AssociatedRecord.reset_column_information
|
40
|
+
DeeplyAssociatedRecord.reset_column_information
|
41
|
+
|
42
|
+
AssociatedRecord.embedded_schema_hashes = {}
|
43
|
+
Record.embedded_schema_hashes = {}
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_schema_changes_on_embedded_association_when_the_cached_object_is_already_in_the_cache_should_request_from_the_db
|
47
|
+
record = Record.fetch(@record.id)
|
48
|
+
record.fetch_associated
|
49
|
+
|
50
|
+
AddColumnToChild.new.up
|
51
|
+
read_new_schema
|
52
|
+
|
53
|
+
# Reloading the association queries
|
54
|
+
# SHOW FULL FIELDS FROM `associated_records`
|
55
|
+
# SHOW TABLES LIKE 'associated_records'
|
56
|
+
# SELECT `associated_records`.* FROM `associated_records` WHERE `associated_records`.`record_id` = 1 ORDER BY id ASC LIMIT 1.
|
57
|
+
assert_queries(3) do
|
58
|
+
assert_nothing_raised { record.fetch_associated.shiny }
|
59
|
+
end
|
60
|
+
|
61
|
+
assert_no_queries { record.fetch_associated.shiny }
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_schema_changes_on_deeply_embedded_association_when_the_cached_object_is_already_in_the_cache_should_request_from_the_db
|
65
|
+
record = Record.fetch(@record.id)
|
66
|
+
associated_record_from_cache = record.fetch_associated
|
67
|
+
associated_record_from_cache.fetch_deeply_associated_records
|
68
|
+
|
69
|
+
AddColumnToDeepChild.new.up
|
70
|
+
read_new_schema
|
71
|
+
|
72
|
+
# Loading association queries
|
73
|
+
# SHOW FULL FIELDS FROM `deeply_associated_records`
|
74
|
+
# SHOW FULL FIELDS FROM `associated_records`
|
75
|
+
# SHOW TABLES LIKE 'deeply_associated_records'
|
76
|
+
# SELECT `deeply_associated_records`.* FROM `deeply_associated_records` WHERE `deeply_associated_records`.`associated_record_id` = 1 ORDER BY name DESC.
|
77
|
+
assert_queries(4) do
|
78
|
+
assert_nothing_raised do
|
79
|
+
associated_record_from_cache.fetch_deeply_associated_records.map(&:new_column)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
assert_no_queries do
|
84
|
+
associated_record_from_cache.fetch_deeply_associated_records.each{ |obj| assert_nil obj.new_column }
|
85
|
+
record.fetch_associated.fetch_deeply_associated_records.each{ |obj| assert_nil obj.new_column }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -6,6 +6,14 @@ require 'helpers/database_connection'
|
|
6
6
|
|
7
7
|
require File.dirname(__FILE__) + '/../lib/identity_cache'
|
8
8
|
|
9
|
+
if ENV['BOXEN_HOME'].present?
|
10
|
+
$memcached_port = 21211
|
11
|
+
$mysql_port = 13306
|
12
|
+
else
|
13
|
+
$memcached_port = 11211
|
14
|
+
$mysql_port = 3306
|
15
|
+
end
|
16
|
+
|
9
17
|
DatabaseConnection.setup
|
10
18
|
|
11
19
|
class IdentityCache::TestCase < MiniTest::Unit::TestCase
|
@@ -35,6 +43,21 @@ class IdentityCache::TestCase < MiniTest::Unit::TestCase
|
|
35
43
|
assert *args
|
36
44
|
end
|
37
45
|
|
46
|
+
def assert_queries(num = 1)
|
47
|
+
counter = SQLCounter.new
|
48
|
+
subscriber = ActiveSupport::Notifications.subscribe('sql.active_record', counter)
|
49
|
+
yield
|
50
|
+
ensure
|
51
|
+
ActiveSupport::Notifications.unsubscribe(subscriber)
|
52
|
+
assert_equal num, counter.log.size, "#{counter.log.size} instead of #{num} queries were executed.#{counter.log.size == 0 ? '' : "\nQueries:\n#{counter.log.join("\n")}"}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def assert_no_queries
|
56
|
+
assert_queries(0) do
|
57
|
+
yield
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
38
61
|
def cache_hash(key)
|
39
62
|
CityHash.hash64(key)
|
40
63
|
end
|
@@ -71,3 +94,29 @@ class IdentityCache::TestCase < MiniTest::Unit::TestCase
|
|
71
94
|
}
|
72
95
|
end
|
73
96
|
end
|
97
|
+
|
98
|
+
class SQLCounter
|
99
|
+
cattr_accessor :ignored_sql
|
100
|
+
self.ignored_sql = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
|
101
|
+
|
102
|
+
# FIXME: this needs to be refactored so specific database can add their own
|
103
|
+
# ignored SQL. This ignored SQL is for Oracle.
|
104
|
+
ignored_sql.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
|
105
|
+
|
106
|
+
attr_reader :ignore
|
107
|
+
attr_accessor :log
|
108
|
+
|
109
|
+
def initialize(ignore = self.class.ignored_sql)
|
110
|
+
@ignore = ignore
|
111
|
+
@log = []
|
112
|
+
end
|
113
|
+
|
114
|
+
def call(name, start, finish, message_id, values)
|
115
|
+
sql = values[:sql]
|
116
|
+
|
117
|
+
# FIXME: this seems bad. we should probably have a better way to indicate
|
118
|
+
# the query was cached
|
119
|
+
return if 'CACHE' == values[:name] || ignore.any? { |x| x =~ sql }
|
120
|
+
self.log << sql
|
121
|
+
end
|
122
|
+
end
|
metadata
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: identity_cache
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.1
|
5
4
|
prerelease:
|
5
|
+
version: 0.0.2
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Camilo Lopez
|
@@ -13,63 +13,152 @@ authors:
|
|
13
13
|
autorequire:
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
|
-
date: 2013-
|
16
|
+
date: 2013-04-08 00:00:00.000000000 Z
|
17
17
|
dependencies:
|
18
18
|
- !ruby/object:Gem::Dependency
|
19
|
+
prerelease: false
|
19
20
|
name: ar_transaction_changes
|
20
|
-
|
21
|
+
type: :runtime
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.0.1
|
21
27
|
none: false
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
22
29
|
requirements:
|
23
|
-
- - =
|
30
|
+
- - '='
|
24
31
|
- !ruby/object:Gem::Version
|
25
32
|
version: 0.0.1
|
33
|
+
none: false
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
prerelease: false
|
36
|
+
name: activerecord
|
26
37
|
type: :runtime
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - '='
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 3.2.13
|
43
|
+
none: false
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - '='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: 3.2.13
|
49
|
+
none: false
|
50
|
+
- !ruby/object:Gem::Dependency
|
27
51
|
prerelease: false
|
28
|
-
|
52
|
+
name: activesupport
|
53
|
+
type: :runtime
|
54
|
+
version_requirements: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - '='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: 3.2.13
|
59
|
+
none: false
|
60
|
+
requirement: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - '='
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: 3.2.13
|
65
|
+
none: false
|
29
66
|
- !ruby/object:Gem::Dependency
|
67
|
+
prerelease: false
|
30
68
|
name: cityhash
|
31
|
-
|
69
|
+
type: :runtime
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - '='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 0.6.0
|
32
75
|
none: false
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
33
77
|
requirements:
|
34
|
-
- - =
|
78
|
+
- - '='
|
35
79
|
- !ruby/object:Gem::Version
|
36
80
|
version: 0.6.0
|
37
|
-
|
81
|
+
none: false
|
82
|
+
- !ruby/object:Gem::Dependency
|
38
83
|
prerelease: false
|
39
|
-
|
84
|
+
name: memcache-client
|
85
|
+
type: :development
|
86
|
+
version_requirements: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ! '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
none: false
|
92
|
+
requirement: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ! '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
none: false
|
40
98
|
- !ruby/object:Gem::Dependency
|
99
|
+
prerelease: false
|
41
100
|
name: rake
|
42
|
-
|
101
|
+
type: :development
|
102
|
+
version_requirements: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ! '>='
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
43
107
|
none: false
|
108
|
+
requirement: !ruby/object:Gem::Requirement
|
44
109
|
requirements:
|
45
110
|
- - ! '>='
|
46
111
|
- !ruby/object:Gem::Version
|
47
112
|
version: '0'
|
48
|
-
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: *70221938343780
|
113
|
+
none: false
|
51
114
|
- !ruby/object:Gem::Dependency
|
115
|
+
prerelease: false
|
52
116
|
name: mocha
|
53
|
-
|
117
|
+
type: :development
|
118
|
+
version_requirements: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ! '>='
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
54
123
|
none: false
|
124
|
+
requirement: !ruby/object:Gem::Requirement
|
55
125
|
requirements:
|
56
126
|
- - ! '>='
|
57
127
|
- !ruby/object:Gem::Version
|
58
128
|
version: '0'
|
59
|
-
|
60
|
-
prerelease: false
|
61
|
-
version_requirements: *70221938343320
|
129
|
+
none: false
|
62
130
|
- !ruby/object:Gem::Dependency
|
131
|
+
prerelease: false
|
63
132
|
name: mysql2
|
64
|
-
|
133
|
+
type: :development
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ! '>='
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
65
139
|
none: false
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
66
141
|
requirements:
|
67
142
|
- - ! '>='
|
68
143
|
- !ruby/object:Gem::Version
|
69
144
|
version: '0'
|
70
|
-
|
145
|
+
none: false
|
146
|
+
- !ruby/object:Gem::Dependency
|
71
147
|
prerelease: false
|
72
|
-
|
148
|
+
name: debugger
|
149
|
+
type: :development
|
150
|
+
version_requirements: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - ! '>='
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0'
|
155
|
+
none: false
|
156
|
+
requirement: !ruby/object:Gem::Requirement
|
157
|
+
requirements:
|
158
|
+
- - ! '>='
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: '0'
|
161
|
+
none: false
|
73
162
|
description: Opt in read through ActiveRecord caching.
|
74
163
|
email:
|
75
164
|
- harry.brundage@shopify.com
|
@@ -79,6 +168,7 @@ extra_rdoc_files: []
|
|
79
168
|
files:
|
80
169
|
- .gitignore
|
81
170
|
- .travis.yml
|
171
|
+
- CHANGELOG
|
82
172
|
- Gemfile
|
83
173
|
- LICENSE
|
84
174
|
- README.md
|
@@ -89,6 +179,7 @@ files:
|
|
89
179
|
- lib/identity_cache/version.rb
|
90
180
|
- lib/memoized_cache_proxy.rb
|
91
181
|
- test/attribute_cache_test.rb
|
182
|
+
- test/cache_fetch_includes_test.rb
|
92
183
|
- test/denormalized_has_many_test.rb
|
93
184
|
- test/denormalized_has_one_test.rb
|
94
185
|
- test/fetch_multi_test.rb
|
@@ -102,6 +193,7 @@ files:
|
|
102
193
|
- test/normalized_has_many_test.rb
|
103
194
|
- test/recursive_denormalized_has_many_test.rb
|
104
195
|
- test/save_test.rb
|
196
|
+
- test/schema_change_test.rb
|
105
197
|
- test/test_helper.rb
|
106
198
|
homepage: https://github.com/Shopify/identity_cache
|
107
199
|
licenses: []
|
@@ -110,26 +202,26 @@ rdoc_options: []
|
|
110
202
|
require_paths:
|
111
203
|
- lib
|
112
204
|
required_ruby_version: !ruby/object:Gem::Requirement
|
113
|
-
none: false
|
114
205
|
requirements:
|
115
206
|
- - ! '>='
|
116
207
|
- !ruby/object:Gem::Version
|
117
|
-
version: '0'
|
118
208
|
segments:
|
119
209
|
- 0
|
120
|
-
hash:
|
121
|
-
|
210
|
+
hash: -901854368761295247
|
211
|
+
version: '0'
|
122
212
|
none: false
|
213
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
214
|
requirements:
|
124
215
|
- - ! '>='
|
125
216
|
- !ruby/object:Gem::Version
|
126
|
-
version: '0'
|
127
217
|
segments:
|
128
218
|
- 0
|
129
|
-
hash:
|
219
|
+
hash: -901854368761295247
|
220
|
+
version: '0'
|
221
|
+
none: false
|
130
222
|
requirements: []
|
131
223
|
rubyforge_project:
|
132
|
-
rubygems_version: 1.8.
|
224
|
+
rubygems_version: 1.8.23
|
133
225
|
signing_key:
|
134
226
|
specification_version: 3
|
135
227
|
summary: IdentityCache lets you specify how you want to cache your model objects,
|
@@ -138,6 +230,7 @@ summary: IdentityCache lets you specify how you want to cache your model objects
|
|
138
230
|
database is only hit when a copy of the object cannot be found in Memcached.
|
139
231
|
test_files:
|
140
232
|
- test/attribute_cache_test.rb
|
233
|
+
- test/cache_fetch_includes_test.rb
|
141
234
|
- test/denormalized_has_many_test.rb
|
142
235
|
- test/denormalized_has_one_test.rb
|
143
236
|
- test/fetch_multi_test.rb
|
@@ -151,4 +244,5 @@ test_files:
|
|
151
244
|
- test/normalized_has_many_test.rb
|
152
245
|
- test/recursive_denormalized_has_many_test.rb
|
153
246
|
- test/save_test.rb
|
247
|
+
- test/schema_change_test.rb
|
154
248
|
- test/test_helper.rb
|