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.
@@ -0,0 +1,3 @@
1
+ 0.0.2
2
+ * Fix: Existent embedded entries will no longer raise when ActiveModel::MissingAttributeError when accessing a newly created attribute.
3
+ * Fix: Do not marshal raw AcriveRecord associations
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', :github => 'Shopify/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(handle)
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.
@@ -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
@@ -12,8 +12,14 @@ module IdentityCache
12
12
  attr_accessor :logger, :readonly
13
13
  attr_reader :cache
14
14
 
15
- def cache_backend=(memcache)
16
- cache.memcache = memcache
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 included(base)
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
- objects_in_order = cache_keys.map {|key| objects_by_key[key] }
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
- all_cached_associations.select{|k, v| v[:embed]}.map do |child_association, options|
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
- child_includes = child_class.respond_to?(:cache_fetch_includes) ? child_class.cache_fetch_includes : []
413
- if child_includes.empty?
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 => child_class.cache_fetch_includes }
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
- column_list = columns.sort_by(&:name).map {|c| "#{c.name}:#{c.type}"} * ","
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.all_cached_associations.each do |cached_association, options|
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
- value = instance_variable_get(ivar_full_name)
481
- return value unless value.nil?
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
 
@@ -1,3 +1,3 @@
1
1
  module IdentityCache
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -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
- @record.expects(:associated => nil)
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.expects(:associated => nil)
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
 
@@ -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)
@@ -1,46 +1,10 @@
1
1
  module Rails
2
- class Cache
3
2
 
4
- def initialize
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
@@ -1,5 +1,6 @@
1
1
  module DatabaseConnection
2
2
  def self.setup
3
+ DATABASE_CONFIG['port'] ||= $mysql_port
3
4
  ActiveRecord::Base.establish_connection(DATABASE_CONFIG)
4
5
  ActiveRecord::Base.connection
5
6
  rescue
@@ -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
- record_from_cache_miss = Record.fetch(@record.id)
59
- expected = @associated_record.deeply_associated_records
60
-
61
- assert record_from_cache_miss.fetch_associated_records[0].deeply_associated_records.loaded?
62
- assert record_from_cache_miss.fetch_associated_records[1].deeply_associated_records.loaded?
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
@@ -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-03-17 00:00:00.000000000 Z
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
- requirement: &70221938344660 !ruby/object:Gem::Requirement
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
- version_requirements: *70221938344660
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
- requirement: &70221938344160 !ruby/object:Gem::Requirement
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
- type: :runtime
81
+ none: false
82
+ - !ruby/object:Gem::Dependency
38
83
  prerelease: false
39
- version_requirements: *70221938344160
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
- requirement: &70221938343780 !ruby/object:Gem::Requirement
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
- type: :development
49
- prerelease: false
50
- version_requirements: *70221938343780
113
+ none: false
51
114
  - !ruby/object:Gem::Dependency
115
+ prerelease: false
52
116
  name: mocha
53
- requirement: &70221938343320 !ruby/object:Gem::Requirement
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
- type: :development
60
- prerelease: false
61
- version_requirements: *70221938343320
129
+ none: false
62
130
  - !ruby/object:Gem::Dependency
131
+ prerelease: false
63
132
  name: mysql2
64
- requirement: &70221938342900 !ruby/object:Gem::Requirement
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
- type: :development
145
+ none: false
146
+ - !ruby/object:Gem::Dependency
71
147
  prerelease: false
72
- version_requirements: *70221938342900
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: 460007319304896635
121
- required_rubygems_version: !ruby/object:Gem::Requirement
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: 460007319304896635
219
+ hash: -901854368761295247
220
+ version: '0'
221
+ none: false
130
222
  requirements: []
131
223
  rubyforge_project:
132
- rubygems_version: 1.8.11
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