identity_cache 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e1ebdc0142ea9597f6053f9d07ef1c43bfbd5434
4
- data.tar.gz: 4aacba1f352ff488dc13cf4eadb136369c5cb767
3
+ metadata.gz: 1fa182ed254be40433e84f2bdef8faf341581e5f
4
+ data.tar.gz: b795a7e03b4cf94a9d5ff3fd77e04186cde84d5c
5
5
  SHA512:
6
- metadata.gz: a46686540738897cc446dda713c3d99dcb8c7fd513351894d7ac3ae3ee43c46baac0648eb0876cb1db1e5b2bfacf1ee2e8586b3aeb055d6126479794fd797dfa
7
- data.tar.gz: 40af995454fc23abaddcab686808a62972758d586f6a4514314a0b30730fac71accbfd83cd671a9e90553dc5daa45a30ab162c21680e76b9691697e9bb8f7707
6
+ metadata.gz: 51c4645eb6a548c720444d6964412576bf9c84a399ad1a100251953d24f7e114654170eb4ae3111c906fedd1d9f0555c1c3c2734904675383f84f9ee62fe3362
7
+ data.tar.gz: fabf846eb3ac2d9ccd9eda7f11dc3e42ed64ac5b678863fd1755e066bc04fc77cc46c4cf9eade1149da4a7fa244c28a45a62fdd4c798501a0b3764ded56d697f
data/.travis.yml CHANGED
@@ -1,8 +1,8 @@
1
1
  language: ruby
2
2
 
3
3
  rvm:
4
- - 2.1
5
- - 2.2.3
4
+ - '2.2.6'
5
+ - '2.3.3'
6
6
 
7
7
  gemfile:
8
8
  - Gemfile.rails42
@@ -10,8 +10,6 @@ gemfile:
10
10
 
11
11
  matrix:
12
12
  exclude:
13
- - rvm: 2.1
14
- gemfile: Gemfile.rails50
15
13
  - gemfile: Gemfile.rails50
16
14
  env: DB=postgresql
17
15
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # IdentityCache changelog
2
2
 
3
+ #### 0.4.1
4
+
5
+ - Deprecated embedded associations on models that don't use IDC (#305)
6
+ - Remove a respond_to? check that hides mistakes in includes hash (#307)
7
+ - Drop ruby 2.1 support (#301)
8
+ - Avoid querying when no ids are passed to fetch_multi (#297)
9
+ - Fix fetching already loaded belongs_to association (#294)
10
+ - Move `should_use_cache?` calls to the model-level (#291)
11
+ - Clone instead of dup record when readonlyifying fetched records (#292)
12
+ - Consistently store the array for cached has many associations (#288)
13
+
3
14
  #### 0.4.0
4
15
 
5
16
  - Return an array from fetched association to prevent chaining. Up to now, a relation was returned by default. (#287)
data/README.md CHANGED
@@ -19,7 +19,7 @@ And then execute:
19
19
  $ bundle
20
20
 
21
21
 
22
- Add the following to your environment/production.rb:
22
+ Add the following to all your environment/*.rb files (production/development/test):
23
23
 
24
24
  ```ruby
25
25
  config.identity_cache_store = :mem_cache_store, Memcached::Rails.new(:servers => ["mem1.server.com"])
@@ -36,6 +36,10 @@ IdentityCache.cache_backend = ActiveSupport::Cache.lookup_store(*Rails.configura
36
36
  ### Basic Usage
37
37
 
38
38
  ``` ruby
39
+ class Image < ActiveRecord::Base
40
+ include IdentityCache::WithoutPrimaryIndex
41
+ end
42
+
39
43
  class Product < ActiveRecord::Base
40
44
  include IdentityCache
41
45
 
@@ -44,10 +48,10 @@ class Product < ActiveRecord::Base
44
48
  cache_has_many :images, :embed => true
45
49
  end
46
50
 
47
- # Fetch the product by its id, the primary index.
51
+ # Fetch the product by its id using the primary primary index as well as the embedded images association.
48
52
  @product = Product.fetch(id)
49
53
 
50
- # Fetch the images for the Product. Images are embedded so the product fetch would have already loaded them.
54
+ # Access the loaded images for the Product.
51
55
  @images = @product.fetch_images
52
56
  ```
53
57
 
data/dev.yml CHANGED
@@ -3,9 +3,7 @@ name: identity-cache
3
3
  up:
4
4
  - homebrew:
5
5
  - postgresql
6
- - ruby:
7
- version: 2.2.3p172-shopify
8
- package: shopify/shopify/shopify-ruby
6
+ - ruby: 2.3.3
9
7
  - railgun
10
8
  - bundler
11
9
 
@@ -15,11 +15,13 @@ Gem::Specification.new do |gem|
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = IdentityCache::VERSION
17
17
 
18
+ gem.required_ruby_version = '>= 2.2.0'
19
+
18
20
  gem.add_dependency('ar_transaction_changes', '~> 1.0')
19
21
  gem.add_dependency('activerecord', '>= 4.2.0')
20
22
 
21
23
  gem.add_development_dependency('memcached', '~> 1.8.0')
22
- gem.add_development_dependency('memcached_store', '~> 0.12.6')
24
+ gem.add_development_dependency('memcached_store', '~> 1.0.0')
23
25
  gem.add_development_dependency('rake')
24
26
  gem.add_development_dependency('mocha', '0.14.0')
25
27
  gem.add_development_dependency('spy')
@@ -7,14 +7,27 @@ require 'identity_cache/memoized_cache_proxy'
7
7
  require 'identity_cache/belongs_to_caching'
8
8
  require 'identity_cache/cache_key_generation'
9
9
  require 'identity_cache/configuration_dsl'
10
+ require 'identity_cache/should_use_cache'
10
11
  require 'identity_cache/parent_model_expiration'
11
12
  require 'identity_cache/query_api'
12
13
  require "identity_cache/cache_hash"
13
14
  require "identity_cache/cache_invalidation"
14
15
  require "identity_cache/cache_fetcher"
15
16
  require "identity_cache/fallback_fetcher"
17
+ require 'identity_cache/without_primary_index'
16
18
 
17
19
  module IdentityCache
20
+ extend ActiveSupport::Concern
21
+
22
+ include ArTransactionChanges
23
+ include IdentityCache::BelongsToCaching
24
+ include IdentityCache::CacheKeyGeneration
25
+ include IdentityCache::ConfigurationDSL
26
+ include IdentityCache::QueryAPI
27
+ include IdentityCache::CacheInvalidation
28
+ include IdentityCache::ShouldUseCache
29
+ include IdentityCache::ParentModelExpiration
30
+
18
31
  CACHED_NIL = :idc_cached_nil
19
32
  BATCH_SIZE = 1000
20
33
  DELETED = :idc_cached_deleted
@@ -55,14 +68,10 @@ module IdentityCache
55
68
  self.fetch_read_only_records = version >= Gem::Version.new("0.5")
56
69
 
57
70
  def included(base) #:nodoc:
58
- raise AlreadyIncludedError if base.include?(IdentityCache::ConfigurationDSL)
59
-
60
- base.send(:include, ArTransactionChanges) unless base.include?(ArTransactionChanges)
61
- base.send(:include, IdentityCache::BelongsToCaching)
62
- base.send(:include, IdentityCache::CacheKeyGeneration)
63
- base.send(:include, IdentityCache::ConfigurationDSL)
64
- base.send(:include, IdentityCache::QueryAPI)
65
- base.send(:include, IdentityCache::CacheInvalidation)
71
+ raise AlreadyIncludedError if base.respond_to?(:cached_model)
72
+ base.class_attribute :cached_model
73
+ base.cached_model = base
74
+ super
66
75
  end
67
76
 
68
77
  # Sets the cache adaptor IdentityCache will be using
@@ -34,15 +34,16 @@ module IdentityCache
34
34
  foreign_key = options[:association_reflection].foreign_key
35
35
  self.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
36
36
  def #{options[:cached_accessor_name]}
37
- if IdentityCache.should_use_cache? && #{foreign_key}.present? && !association(:#{association}).loaded?
37
+ association_klass = association(:#{association}).klass
38
+ if association_klass.should_use_cache? && #{foreign_key}.present? && !association(:#{association}).loaded?
38
39
  if instance_variable_defined?(:@#{options[:records_variable_name]})
39
40
  @#{options[:records_variable_name]}
40
41
  else
41
- @#{options[:records_variable_name]} = association(:#{association}).klass.fetch_by_id(#{foreign_key})
42
+ @#{options[:records_variable_name]} = association_klass.fetch_by_id(#{foreign_key})
42
43
  end
43
44
  else
44
- if IdentityCache.fetch_read_only_records && IdentityCache.should_use_cache?
45
- load_and_readonlyify(:#{association})
45
+ if IdentityCache.fetch_read_only_records && association_klass.should_use_cache?
46
+ readonly_copy(association(:#{association}).load_target)
46
47
  else
47
48
  #{association}
48
49
  end
@@ -3,17 +3,17 @@ module IdentityCache
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do |base|
6
- base.class_attribute :cached_model
7
6
  base.class_attribute :cache_indexes
8
7
  base.class_attribute :cached_has_manys
9
8
  base.class_attribute :cached_has_ones
10
9
  base.class_attribute :primary_cache_index_enabled
11
10
 
12
- base.cached_model = base
13
11
  base.cached_has_manys = {}
14
12
  base.cached_has_ones = {}
15
13
  base.cache_indexes = []
16
14
  base.primary_cache_index_enabled = true
15
+
16
+ base.after_commit :expire_parent_caches
17
17
  end
18
18
 
19
19
  module ClassMethods
@@ -175,6 +175,7 @@ module IdentityCache
175
175
  end
176
176
 
177
177
  def disable_primary_cache_index
178
+ ActiveSupport::Deprecation.warn("disable_primary_cache_index is deprecated, use `include IdentityCache::WithoutPrimaryIndex` instead")
178
179
  ensure_base_model
179
180
  self.primary_cache_index_enabled = false
180
181
  end
@@ -233,7 +234,8 @@ module IdentityCache
233
234
  end
234
235
 
235
236
  def #{options[:cached_accessor_name]}
236
- if IdentityCache.should_use_cache? && !#{association}.loaded?
237
+ association_klass = association(:#{association}).klass
238
+ if association_klass.should_use_cache? && !#{association}.loaded?
237
239
  @#{options[:records_variable_name]} ||= #{options[:association_reflection].klass}.fetch_multi(#{options[:cached_ids_name]})
238
240
  else
239
241
  #{association}.to_a
@@ -251,24 +253,33 @@ module IdentityCache
251
253
 
252
254
  def attribute_dynamic_fetcher(attribute, fields, values, unique_index) #:nodoc:
253
255
  raise_if_scoped
254
- cache_key = rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values, unique_index)
255
- IdentityCache.fetch(cache_key) do
256
- query = reorder(nil).where(Hash[fields.zip(values)])
257
- query = query.limit(1) if unique_index
258
- results = query.pluck(attribute)
259
- unique_index ? results.first : results
256
+
257
+ if should_use_cache?
258
+ cache_key = rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values, unique_index)
259
+ IdentityCache.fetch(cache_key) do
260
+ dynamic_attribute_cache_miss(attribute, fields, values, unique_index)
261
+ end
262
+ else
263
+ dynamic_attribute_cache_miss(attribute, fields, values, unique_index)
260
264
  end
261
265
  end
262
266
 
267
+ def dynamic_attribute_cache_miss(attribute, fields, values, unique_index)
268
+ query = reorder(nil).where(Hash[fields.zip(values)])
269
+ query = query.limit(1) if unique_index
270
+ results = query.pluck(attribute)
271
+ unique_index ? results.first : results
272
+ end
273
+
263
274
  def add_parent_expiry_hook(options)
264
275
  child_class = options[:association_reflection].klass
265
-
266
- child_class.send(:include, ArTransactionChanges) unless child_class.include?(ArTransactionChanges)
267
- child_class.send(:include, ParentModelExpiration) unless child_class.include?(ParentModelExpiration)
268
-
276
+ unless child_class < IdentityCache
277
+ message = "associated class #{child_class} will need to include IdentityCache or " \
278
+ "IdentityCache::WithoutPrimaryIndex for embedded associations"
279
+ ActiveSupport::Deprecation.warn(message, caller(3))
280
+ child_class.send(:include, IdentityCache::WithoutPrimaryIndex)
281
+ end
269
282
  child_class.parent_expiration_entries[options[:inverse_name]] << [self, options[:only_on_foreign_key_change]]
270
-
271
- child_class.after_commit :expire_parent_caches
272
283
  end
273
284
 
274
285
  def deprecate_embed_option(options, old_value, new_value)
@@ -25,7 +25,7 @@ module IdentityCache
25
25
  key = record.primary_cache_index_key
26
26
  unless parents_to_expire[key]
27
27
  parents_to_expire[key] = record
28
- record.add_parents_to_cache_expiry_set(parents_to_expire) if record.respond_to?(:add_parents_to_cache_expiry_set, true)
28
+ record.add_parents_to_cache_expiry_set(parents_to_expire)
29
29
  end
30
30
  end
31
31
 
@@ -21,7 +21,7 @@ module IdentityCache
21
21
  raise_if_scoped
22
22
  raise NotImplementedError, "fetching needs the primary index enabled" unless primary_cache_index_enabled
23
23
  return unless id
24
- record = if IdentityCache.should_use_cache?
24
+ record = if should_use_cache?
25
25
  require_if_necessary do
26
26
  object = nil
27
27
  coder = IdentityCache.fetch(rails_cache_key(id)){ coder_from_record(object = resolve_cache_miss(id)) }
@@ -53,7 +53,7 @@ module IdentityCache
53
53
  raise NotImplementedError, "fetching needs the primary index enabled" unless primary_cache_index_enabled
54
54
  options = ids.extract_options!
55
55
  ids.flatten!(1)
56
- records = if IdentityCache.should_use_cache?
56
+ records = if should_use_cache?
57
57
  require_if_necessary do
58
58
  cache_keys = ids.map {|id| rails_cache_key(id) }
59
59
  key_to_id_map = Hash[ cache_keys.zip(ids) ]
@@ -79,10 +79,6 @@ module IdentityCache
79
79
  def prefetch_associations(associations, records)
80
80
  records = records.to_a
81
81
  return if records.empty?
82
- unless IdentityCache.should_use_cache?
83
- ActiveRecord::Associations::Preloader.new.preload(records, associations)
84
- return
85
- end
86
82
 
87
83
  case associations
88
84
  when nil
@@ -97,8 +93,8 @@ module IdentityCache
97
93
  associations.each do |association, sub_associations|
98
94
  next_level_records = prefetch_one_association(association, records)
99
95
 
100
- associated_class = reflect_on_association(association).klass
101
- if associated_class.respond_to?(:prefetch_associations)
96
+ if sub_associations.present?
97
+ associated_class = reflect_on_association(association).klass
102
98
  associated_class.prefetch_associations(sub_associations, next_level_records)
103
99
  end
104
100
  end
@@ -152,16 +148,17 @@ module IdentityCache
152
148
  value = if IdentityCache.unmap_cached_nil_for(coder_or_array).nil?
153
149
  nil
154
150
  elsif (reflection = record.class.reflect_on_association(association_name)).collection?
155
- association = reflection.association_class.new(record, reflection)
156
- association.target = coder_or_array.map {|e| record_from_coder(e) }
151
+ associated_records = coder_or_array.map {|e| record_from_coder(e) }
157
152
 
158
- set_inverse_of_cached_has_many(record, reflection, association.target)
153
+ set_inverse_of_cached_has_many(record, reflection, associated_records)
159
154
 
160
155
  unless IdentityCache.never_set_inverse_association
156
+ association = reflection.association_class.new(record, reflection)
157
+ association.target = associated_records
161
158
  association.target.each {|e| association.set_inverse_instance(e) }
162
159
  end
163
160
 
164
- association
161
+ associated_records
165
162
  else
166
163
  record_from_coder(coder_or_array)
167
164
  end
@@ -227,7 +224,7 @@ module IdentityCache
227
224
  record = self.includes(cache_fetch_includes).reorder(nil).where(primary_key => id).first
228
225
  if record
229
226
  preload_id_embedded_associations([record])
230
- record.readonly! if IdentityCache.fetch_read_only_records && IdentityCache.should_use_cache?
227
+ record.readonly! if IdentityCache.fetch_read_only_records && should_use_cache?
231
228
  end
232
229
  record
233
230
  end
@@ -286,10 +283,7 @@ module IdentityCache
286
283
  associations_for_identity_cache = recursively_embedded_associations.map do |child_association, options|
287
284
  child_class = reflect_on_association(child_association).try(:klass)
288
285
 
289
- child_includes = nil
290
- if child_class.respond_to?(:cache_fetch_includes, true)
291
- child_includes = child_class.send(:cache_fetch_includes)
292
- end
286
+ child_includes = child_class.send(:cache_fetch_includes)
293
287
 
294
288
  if child_includes.blank?
295
289
  child_association
@@ -302,10 +296,12 @@ module IdentityCache
302
296
  end
303
297
 
304
298
  def find_batch(ids)
299
+ return [] if ids.empty?
300
+
305
301
  @id_column ||= columns.detect {|c| c.name == primary_key}
306
302
  ids = ids.map{ |id| connection.type_cast(id, @id_column) }
307
303
  records = where(primary_key => ids).includes(cache_fetch_includes).to_a
308
- records.each(&:readonly!) if IdentityCache.fetch_read_only_records && IdentityCache.should_use_cache?
304
+ records.each(&:readonly!) if IdentityCache.fetch_read_only_records && should_use_cache?
309
305
  preload_id_embedded_associations(records)
310
306
  records_by_id = records.index_by(&:id)
311
307
  ids.map{ |id| records_by_id[id] }
@@ -345,6 +341,11 @@ module IdentityCache
345
341
  end
346
342
 
347
343
  def prefetch_one_association(association, records)
344
+ unless records.first.class.should_use_cache?
345
+ ActiveRecord::Associations::Preloader.new.preload(records, association)
346
+ return
347
+ end
348
+
348
349
  case
349
350
  when details = cached_has_manys[association]
350
351
  prefetch_embedded_association(records, association, details)
@@ -417,26 +418,22 @@ module IdentityCache
417
418
  private
418
419
 
419
420
  def fetch_recursively_cached_association(ivar_name, association_name) # :nodoc:
420
- assoc = if IdentityCache.should_use_cache?
421
- ivar_full_name = :"@#{ivar_name}"
421
+ ivar_full_name = :"@#{ivar_name}"
422
+ assoc = association(association_name)
422
423
 
423
- assoc = if instance_variable_defined?(ivar_full_name)
424
+ if assoc.klass.should_use_cache?
425
+ if instance_variable_defined?(ivar_full_name)
424
426
  instance_variable_get(ivar_full_name)
425
427
  else
426
- cached_assoc = if IdentityCache.fetch_read_only_records
427
- load_and_readonlyify(association_name)
428
- else
429
- send(association_name)
428
+ cached_assoc = assoc.load_target
429
+ if IdentityCache.fetch_read_only_records
430
+ cached_assoc = readonly_copy(cached_assoc)
430
431
  end
431
432
  instance_variable_set(ivar_full_name, cached_assoc)
432
433
  end
433
-
434
- assoc.is_a?(ActiveRecord::Associations::CollectionAssociation) ? assoc.reader : assoc
435
434
  else
436
- send(association_name.to_sym)
435
+ assoc.load_target
437
436
  end
438
- assoc = assoc.to_ary if assoc.respond_to?(:to_ary)
439
- assoc
440
437
  end
441
438
 
442
439
  def expire_primary_index # :nodoc:
@@ -483,13 +480,17 @@ module IdentityCache
483
480
  !destroyed? && transaction_changed_attributes.has_key?(pk) && transaction_changed_attributes[pk].nil?
484
481
  end
485
482
 
486
- def load_and_readonlyify(association_name)
487
- record_or_records = send(association_name)
483
+ def readonly_record_copy(record)
484
+ record = record.clone
485
+ record.readonly!
486
+ record
487
+ end
488
488
 
489
- if self.class.reflect_on_association(association_name).collection?
490
- record_or_records.map { |p| p.dup.tap(&:readonly!) }
491
- else
492
- record_or_records.dup.tap(&:readonly!) if record_or_records
489
+ def readonly_copy(record_or_records)
490
+ if record_or_records.is_a?(Array)
491
+ record_or_records.map { |record| readonly_record_copy(record) }
492
+ elsif record_or_records
493
+ readonly_record_copy(record_or_records)
493
494
  end
494
495
  end
495
496
  end
@@ -0,0 +1,11 @@
1
+ module IdentityCache
2
+ module ShouldUseCache
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def should_use_cache?
7
+ IdentityCache.should_use_cache?
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,4 +1,4 @@
1
1
  module IdentityCache
2
- VERSION = "0.4.0"
2
+ VERSION = "0.4.1"
3
3
  CACHE_VERSION = 6
4
4
  end
@@ -0,0 +1,10 @@
1
+ module IdentityCache
2
+ module WithoutPrimaryIndex
3
+ extend ActiveSupport::Concern
4
+
5
+ included do |base|
6
+ base.send(:include, IdentityCache)
7
+ base.primary_cache_index_enabled = false
8
+ end
9
+ end
10
+ end
@@ -14,7 +14,10 @@ require File.dirname(__FILE__) + '/../test/helpers/database_connection'
14
14
 
15
15
  IdentityCache.logger = Logger.new(nil)
16
16
  IdentityCache.cache_backend = ActiveSupport::Cache::MemcachedStore.new("localhost:#{$memcached_port}", :support_cas => true)
17
- ActiveRecord::Base.raise_in_transactional_callbacks = true
17
+
18
+ if ActiveRecord.gem_version < Gem::Version.new('5') && ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks=)
19
+ ActiveRecord::Base.raise_in_transactional_callbacks = true
20
+ end
18
21
 
19
22
  def create_record(id)
20
23
  Item.new(id)
@@ -31,10 +31,10 @@ class AttributeCacheTest < IdentityCache::TestCase
31
31
  end
32
32
 
33
33
  def test_nil_is_stored_in_the_cache_on_cache_misses
34
- assert_equal nil, AssociatedRecord.fetch_name_by_id(2)
34
+ assert_nil AssociatedRecord.fetch_name_by_id(2)
35
35
 
36
36
  assert_queries(0) do
37
- assert_equal nil, AssociatedRecord.fetch_name_by_id(2)
37
+ assert_nil AssociatedRecord.fetch_name_by_id(2)
38
38
  end
39
39
  end
40
40
 
@@ -63,13 +63,13 @@ class AttributeCacheTest < IdentityCache::TestCase
63
63
 
64
64
  @record.destroy
65
65
 
66
- assert_queries(1) { assert_equal nil, AssociatedRecord.fetch_name_by_id(1) }
66
+ assert_queries(1) { assert_nil AssociatedRecord.fetch_name_by_id(1) }
67
67
  end
68
68
 
69
69
  def test_cached_attribute_values_are_expired_from_the_cache_when_a_new_record_is_saved
70
70
  new_id = 2
71
- assert_queries(1) { assert_equal nil, AssociatedRecord.fetch_name_by_id(new_id) }
72
- assert_queries(0) { assert_equal nil, AssociatedRecord.fetch_name_by_id(new_id) }
71
+ assert_queries(1) { assert_nil AssociatedRecord.fetch_name_by_id(new_id) }
72
+ assert_queries(0) { assert_nil AssociatedRecord.fetch_name_by_id(new_id) }
73
73
 
74
74
  @parent.associated_records.create(:name => 'bar')
75
75
 
@@ -87,7 +87,7 @@ class AttributeCacheTest < IdentityCache::TestCase
87
87
  end
88
88
 
89
89
  def test_previously_stored_cached_nils_are_busted_by_new_record_saves
90
- assert_equal nil, AssociatedRecord.fetch_name_by_id(2)
90
+ assert_nil AssociatedRecord.fetch_name_by_id(2)
91
91
  AssociatedRecord.create(:name => "Jim")
92
92
  assert_equal "Jim", AssociatedRecord.fetch_name_by_id(2)
93
93
  end
@@ -97,4 +97,14 @@ class AttributeCacheTest < IdentityCache::TestCase
97
97
  StiRecordTypeA.cache_attribute :name
98
98
  end
99
99
  end
100
+
101
+ def test_cache_attribute_respects_should_use_cache
102
+ AssociatedRecord.stubs(:should_use_cache?).returns(false)
103
+
104
+ assert_queries(1) do
105
+ assert_memcache_operations(0) do
106
+ AssociatedRecord.fetch_name_by_id(@record.id)
107
+ end
108
+ end
109
+ end
100
110
  end
@@ -33,6 +33,7 @@ class CacheFetchIncludesTest < IdentityCache::TestCase
33
33
 
34
34
  def test_multiple_cached_associations_and_child_associations_are_included_in_includes
35
35
  Item.send(:cache_has_many, :associated_records, :embed => true)
36
+ PolymorphicRecord.send(:include, IdentityCache::WithoutPrimaryIndex)
36
37
  Item.send(:cache_has_many, :polymorphic_records, {:inverse_name => :owner, :embed => true})
37
38
  Item.send(:cache_has_one, :associated, :embed => true)
38
39
  AssociatedRecord.send(:cache_has_many, :deeply_associated_records, :embed => true)
@@ -3,6 +3,7 @@ require "test_helper"
3
3
  class DeeplyNestedAssociatedRecordHasOneTest < IdentityCache::TestCase
4
4
  def test_deeply_nested_models_can_cache_has_one_associations
5
5
  assert_nothing_raised do
6
+ PolymorphicRecord.include(IdentityCache::WithoutPrimaryIndex)
6
7
  Deeply::Nested::AssociatedRecord.has_one :polymorphic_record, as: 'owner'
7
8
  Deeply::Nested::AssociatedRecord.cache_has_one :polymorphic_record, inverse_name: :owner
8
9
  end
@@ -3,6 +3,7 @@ require "test_helper"
3
3
  class DenormalizedHasManyTest < IdentityCache::TestCase
4
4
  def setup
5
5
  super
6
+ PolymorphicRecord.include(IdentityCache::WithoutPrimaryIndex)
6
7
  Item.cache_has_many :associated_records, :embed => true
7
8
 
8
9
  @record = Item.new(:title => 'foo')
@@ -21,7 +22,7 @@ class DenormalizedHasManyTest < IdentityCache::TestCase
21
22
  expected = @record.associated_records
22
23
  record_from_db = Item.find(@record.id)
23
24
 
24
- Item.any_instance.expects(:associated_records).returns(expected)
25
+ Item.any_instance.expects(:association).with(:associated_records).returns(expected)
25
26
 
26
27
  assert_equal @record, record_from_db
27
28
  assert_equal expected, record_from_db.fetch_associated_records
@@ -42,7 +43,11 @@ class DenormalizedHasManyTest < IdentityCache::TestCase
42
43
  assert_equal @record, record_from_cache_hit
43
44
 
44
45
  expected = @record.associated_records
45
- Item.any_instance.expects(:associated_records).never
46
+
47
+ assoc = mock()
48
+ assoc.expects(:klass).returns(Item)
49
+ Item.any_instance.expects(:association).with(:associated_records).returns(assoc).once
50
+
46
51
  assert_equal expected, record_from_cache_hit.fetch_associated_records
47
52
  end
48
53
 
@@ -167,6 +172,17 @@ class DenormalizedHasManyTest < IdentityCache::TestCase
167
172
  end
168
173
  end
169
174
 
175
+ def test_respect_should_use_cache_from_embedded_records
176
+ Item.fetch(@record.id)
177
+ AssociatedRecord.stubs(:should_use_cache?).returns(false)
178
+
179
+ assert_memcache_operations(1) do
180
+ assert_queries(1) do
181
+ Item.fetch(@record.id).fetch_associated_records
182
+ end
183
+ end
184
+ end
185
+
170
186
  class CheckAssociationTest < IdentityCache::TestCase
171
187
  def test_unsupported_through_assocation
172
188
  assert_raises IdentityCache::UnsupportedAssociationError, "caching through associations isn't supported" do
@@ -3,6 +3,7 @@ require "test_helper"
3
3
  class DenormalizedHasOneTest < IdentityCache::TestCase
4
4
  def setup
5
5
  super
6
+ PolymorphicRecord.include(IdentityCache::WithoutPrimaryIndex)
6
7
  Item.cache_has_one :associated
7
8
  Item.cache_index :title, :unique => true
8
9
  @record = Item.new(:title => 'foo')
@@ -265,6 +265,16 @@ class FetchMultiTest < IdentityCache::TestCase
265
265
  end
266
266
  end
267
267
 
268
+ def test_fetch_multi_with_no_keys_does_not_query_when_cache_is_disabled
269
+ Item.stubs(:should_use_cache?).returns(false)
270
+
271
+ assert_queries(0) do
272
+ assert_memcache_operations(0) do
273
+ Item.fetch_multi
274
+ end
275
+ end
276
+ end
277
+
268
278
  private
269
279
 
270
280
  def populate_only_fred
data/test/fetch_test.rb CHANGED
@@ -17,7 +17,7 @@ class FetchTest < IdentityCache::TestCase
17
17
  end
18
18
 
19
19
  def test_fetch_with_garbage_input
20
- assert_equal nil, Item.fetch_by_id('garbage')
20
+ assert_nil Item.fetch_by_id('garbage')
21
21
  end
22
22
 
23
23
  def test_fetch_cache_hit
@@ -110,7 +110,7 @@ class FetchTest < IdentityCache::TestCase
110
110
  nonexistent_record_id = 10
111
111
  fetcher.expects(:add).with(@blob_key + '0', IdentityCache::CACHED_NIL)
112
112
 
113
- assert_equal nil, Item.fetch_by_id(nonexistent_record_id)
113
+ assert_nil Item.fetch_by_id(nonexistent_record_id)
114
114
  end
115
115
 
116
116
  def test_fetch_not_found_should_raise
@@ -123,7 +123,7 @@ class FetchTest < IdentityCache::TestCase
123
123
  def test_cached_nil_expiry_on_record_creation
124
124
  key = @record.primary_cache_index_key
125
125
 
126
- assert_equal nil, Item.fetch_by_id(@record.id)
126
+ assert_nil Item.fetch_by_id(@record.id)
127
127
  assert_equal IdentityCache::CACHED_NIL, backend.read(key)
128
128
 
129
129
  @record.save!
@@ -162,10 +162,10 @@ class FetchTest < IdentityCache::TestCase
162
162
  Item.connection.expects(:exec_query).once.returns(ActiveRecord::Result.new([], []))
163
163
  add = Spy.on(fetcher, :add).and_call_through
164
164
  fetch = Spy.on(fetcher, :fetch).and_call_through
165
- assert_equal nil, Item.fetch_by_title('bob') # exec_query => nil
165
+ assert_nil Item.fetch_by_title('bob') # exec_query => nil
166
166
 
167
- assert_equal nil, Item.fetch_by_title('bob') # returns cached nil
168
- assert_equal nil, Item.fetch_by_title('bob') # returns cached nil
167
+ assert_nil Item.fetch_by_title('bob') # returns cached nil
168
+ assert_nil Item.fetch_by_title('bob') # returns cached nil
169
169
 
170
170
  assert add.has_been_called_with?(@index_key, IdentityCache::CACHED_NIL)
171
171
  assert_equal 3, fetch.calls.length
@@ -244,4 +244,15 @@ class FetchTest < IdentityCache::TestCase
244
244
  end
245
245
  end
246
246
  end
247
+
248
+ def test_respects_should_use_cache_on_record
249
+ @record.save
250
+ Item.stubs(:should_use_cache?).returns(false)
251
+
252
+ assert_memcache_operations(0) do
253
+ assert_queries(1) do
254
+ Item.fetch_by_id(@record.id)
255
+ end
256
+ end
257
+ end
247
258
  end
@@ -18,7 +18,15 @@ module DatabaseConnection
18
18
 
19
19
  def self.drop_tables
20
20
  TABLES.keys.each do |table|
21
- ActiveRecord::Base.connection.drop_table(table) if ActiveRecord::Base.connection.table_exists?(table)
21
+ ActiveRecord::Base.connection.drop_table(table) if table_exists?(table)
22
+ end
23
+ end
24
+
25
+ def self.table_exists?(table)
26
+ if ActiveRecord::Base.connection.respond_to?(:data_source_exists?)
27
+ ActiveRecord::Base.connection.data_source_exists?(table)
28
+ else
29
+ ActiveRecord::Base.connection.table_exists?(table)
22
30
  end
23
31
  end
24
32
 
@@ -2,14 +2,17 @@ require "test_helper"
2
2
 
3
3
  class IdentityCacheTest < IdentityCache::TestCase
4
4
 
5
- class BadModel < ActiveRecord::Base
5
+ class BadModelBase < ActiveRecord::Base
6
+ include IdentityCache
7
+ end
8
+
9
+ class BadModel < BadModelBase
6
10
  end
7
11
 
8
12
  def test_identity_cache_raises_if_loaded_twice
9
13
  assert_raises(IdentityCache::AlreadyIncludedError) do
10
14
  BadModel.class_eval do
11
15
  include IdentityCache
12
- include IdentityCache
13
16
  end
14
17
  end
15
18
  end
@@ -25,12 +25,12 @@ class IndexCacheTest < IdentityCache::TestCase
25
25
  .with(regexp_matches(/ LIMIT 1\Z/i), any_parameters)
26
26
  .returns(ActiveRecord::Result.new([], []))
27
27
 
28
- assert_equal nil, Item.fetch_by_title_and_id('title', '2')
28
+ assert_nil Item.fetch_by_title_and_id('title', '2')
29
29
  end
30
30
 
31
31
  def test_unique_index_caches_nil
32
32
  Item.cache_index :title, :unique => true
33
- assert_equal nil, Item.fetch_by_title('bob')
33
+ assert_nil Item.fetch_by_title('bob')
34
34
  assert_equal IdentityCache::CACHED_NIL, backend.read(cache_key(unique: true))
35
35
  end
36
36
 
@@ -30,7 +30,7 @@ class MemoizedCacheProxyTest < IdentityCache::TestCase
30
30
 
31
31
  IdentityCache.cache.with_memoization do
32
32
  IdentityCache.cache.write('foo', nil)
33
- assert_equal nil, IdentityCache.cache.fetch('foo')
33
+ assert_nil IdentityCache.cache.fetch('foo')
34
34
  IdentityCache.cache.write('bar', false)
35
35
  assert_equal false, IdentityCache.cache.fetch('bar')
36
36
  end
@@ -43,8 +43,8 @@ class NormalizedBelongsToTest < IdentityCache::TestCase
43
43
 
44
44
  def test_fetching_the_association_should_cache_nil_and_not_raise_if_the_record_cant_be_found
45
45
  Item.expects(:fetch_by_id).with(@parent_record.id).returns(nil)
46
- assert_equal nil, @record.fetch_item # miss
47
- assert_equal nil, @record.fetch_item # hit
46
+ assert_nil @record.fetch_item # miss
47
+ assert_nil @record.fetch_item # hit
48
48
  end
49
49
 
50
50
  def test_cache_belongs_to_on_derived_model_raises
@@ -76,6 +76,15 @@ class NormalizedBelongsToTest < IdentityCache::TestCase
76
76
  end
77
77
  end
78
78
 
79
+ def test_db_returned_record_should_never_be_readonly
80
+ IdentityCache.with_fetch_read_only_records do
81
+ uncached_record = @record.item
82
+ refute uncached_record.readonly?
83
+ @record.fetch_item
84
+ refute uncached_record.readonly?
85
+ end
86
+ end
87
+
79
88
  def test_returned_record_with_open_transactions_should_not_be_readonly
80
89
  IdentityCache.with_fetch_read_only_records do
81
90
  Item.transaction do
@@ -84,4 +93,15 @@ class NormalizedBelongsToTest < IdentityCache::TestCase
84
93
  end
85
94
  end
86
95
  end
96
+
97
+ def test_respects_should_use_cache_on_parent
98
+ @record.reload
99
+ @parent_record.class.stubs(:should_use_cache?).returns(false)
100
+
101
+ assert_queries(1) do
102
+ assert_memcache_operations(0) do
103
+ @record.fetch_item
104
+ end
105
+ end
106
+ end
87
107
  end
@@ -217,4 +217,15 @@ class NormalizedHasManyTest < IdentityCache::TestCase
217
217
  assert record_from_cache_miss.fetch_associated_records.all?(&:readonly?)
218
218
  end
219
219
  end
220
+
221
+ def test_respects_should_use_cache_on_association
222
+ @record.reload
223
+ AssociatedRecord.stubs(:should_use_cache?).returns(false)
224
+
225
+ assert_queries(1) do
226
+ assert_memcache_operations(0) do
227
+ @record.fetch_associated_records
228
+ end
229
+ end
230
+ end
220
231
  end
@@ -96,7 +96,7 @@ class PrefetchAssociationsTest < IdentityCache::TestCase
96
96
  def test_prefetch_associations_with_nil_cached_belongs_to
97
97
  Item.send(:cache_belongs_to, :item)
98
98
  @bob.update_attributes!(item_id: 1234)
99
- assert_equal nil, @bob.fetch_item
99
+ assert_nil @bob.fetch_item
100
100
 
101
101
  assert_no_queries do
102
102
  assert_memcache_operations(0) do
@@ -188,6 +188,7 @@ class PrefetchAssociationsTest < IdentityCache::TestCase
188
188
  end
189
189
 
190
190
  def test_fetch_multi_batch_fetches_first_level_associations_who_dont_include_identity_cache
191
+ NotCachedRecord.include(IdentityCache::WithoutPrimaryIndex)
191
192
  Item.send(:cache_has_many, :not_cached_records, :embed => true)
192
193
 
193
194
  @bob_child = @bob.not_cached_records.create!(:name => "bob child")
@@ -35,7 +35,7 @@ class ReadonlyTest < IdentityCache::TestCase
35
35
  def test_clear_should_update_cache
36
36
  backend.write(@key, @value)
37
37
  fetcher.clear
38
- assert_equal nil, backend.read(@key)
38
+ assert_nil backend.read(@key)
39
39
  end
40
40
 
41
41
  def test_fetch_should_not_update_cache
@@ -75,6 +75,7 @@ class SchemaChangeTest < IdentityCache::TestCase
75
75
  def test_schema_changes_on_new_cached_child_association
76
76
  record = Item.fetch(@record.id)
77
77
 
78
+ PolymorphicRecord.include(IdentityCache::WithoutPrimaryIndex)
78
79
  Item.cache_has_many :polymorphic_records, :inverse_name => :owner, :embed => true
79
80
  read_new_schema
80
81
 
@@ -92,6 +93,7 @@ class SchemaChangeTest < IdentityCache::TestCase
92
93
  teardown_models
93
94
  setup_models
94
95
 
96
+ PolymorphicRecord.include(IdentityCache::WithoutPrimaryIndex)
95
97
  Item.cache_has_many :polymorphic_records, :inverse_name => :owner, :embed => true
96
98
  read_new_schema
97
99
 
data/test/test_helper.rb CHANGED
@@ -14,15 +14,15 @@ DatabaseConnection.setup
14
14
  ActiveSupport::Cache::Store.instrument = true if ActiveSupport.version < Gem::Version.new("4.2.0")
15
15
 
16
16
  # This patches AR::MemcacheStore to notify AS::Notifications upon read_multis like the rest of rails does
17
- class ActiveSupport::Cache::MemcachedStore
18
- def read_multi_with_instrumentation(*args, &block)
19
- instrument("read_multi", "MULTI", {:keys => args}) do
20
- read_multi_without_instrumentation(*args, &block)
17
+ module MemcachedStoreInstrumentation
18
+ def read_multi(*args, &block)
19
+ instrument('read_multi', 'MULTI', keys: args) do
20
+ super(*args, &block)
21
21
  end
22
22
  end
23
-
24
- alias_method_chain :read_multi, :instrumentation
25
23
  end
24
+ ActiveSupport::Cache::MemcachedStore.prepend(MemcachedStoreInstrumentation)
25
+
26
26
 
27
27
  MiniTest::Test = MiniTest::Unit::TestCase unless defined?(MiniTest::Test)
28
28
  class IdentityCache::TestCase < Minitest::Test
@@ -30,7 +30,7 @@ class IdentityCache::TestCase < Minitest::Test
30
30
  attr_reader :backend
31
31
 
32
32
  def setup
33
- if ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks=)
33
+ if ActiveRecord.gem_version < Gem::Version.new('5') && ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks=)
34
34
  ActiveRecord::Base.raise_in_transactional_callbacks = true
35
35
  end
36
36
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: identity_cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Camilo Lopez
@@ -64,14 +64,14 @@ dependencies:
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: 0.12.6
67
+ version: 1.0.0
68
68
  type: :development
69
69
  prerelease: false
70
70
  version_requirements: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: 0.12.6
74
+ version: 1.0.0
75
75
  - !ruby/object:Gem::Dependency
76
76
  name: rake
77
77
  requirement: !ruby/object:Gem::Requirement
@@ -214,7 +214,9 @@ files:
214
214
  - lib/identity_cache/memoized_cache_proxy.rb
215
215
  - lib/identity_cache/parent_model_expiration.rb
216
216
  - lib/identity_cache/query_api.rb
217
+ - lib/identity_cache/should_use_cache.rb
217
218
  - lib/identity_cache/version.rb
219
+ - lib/identity_cache/without_primary_index.rb
218
220
  - performance/cache_runner.rb
219
221
  - performance/cpu.rb
220
222
  - performance/externals.rb
@@ -260,7 +262,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
260
262
  requirements:
261
263
  - - ">="
262
264
  - !ruby/object:Gem::Version
263
- version: '0'
265
+ version: 2.2.0
264
266
  required_rubygems_version: !ruby/object:Gem::Requirement
265
267
  requirements:
266
268
  - - ">="