identity_cache 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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
  - - ">="