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 +4 -4
- data/.travis.yml +2 -4
- data/CHANGELOG.md +11 -0
- data/README.md +7 -3
- data/dev.yml +1 -3
- data/identity_cache.gemspec +3 -1
- data/lib/identity_cache.rb +17 -8
- data/lib/identity_cache/belongs_to_caching.rb +5 -4
- data/lib/identity_cache/configuration_dsl.rb +26 -15
- data/lib/identity_cache/parent_model_expiration.rb +1 -1
- data/lib/identity_cache/query_api.rb +37 -36
- data/lib/identity_cache/should_use_cache.rb +11 -0
- data/lib/identity_cache/version.rb +1 -1
- data/lib/identity_cache/without_primary_index.rb +10 -0
- data/performance/cache_runner.rb +4 -1
- data/test/attribute_cache_test.rb +16 -6
- data/test/cache_fetch_includes_test.rb +1 -0
- data/test/deeply_nested_associated_record_test.rb +1 -0
- data/test/denormalized_has_many_test.rb +18 -2
- data/test/denormalized_has_one_test.rb +1 -0
- data/test/fetch_multi_test.rb +10 -0
- data/test/fetch_test.rb +17 -6
- data/test/helpers/database_connection.rb +9 -1
- data/test/identity_cache_test.rb +5 -2
- data/test/index_cache_test.rb +2 -2
- data/test/memoized_cache_proxy_test.rb +1 -1
- data/test/normalized_belongs_to_test.rb +22 -2
- data/test/normalized_has_many_test.rb +11 -0
- data/test/prefetch_associations_test.rb +2 -1
- data/test/readonly_test.rb +1 -1
- data/test/schema_change_test.rb +2 -0
- data/test/test_helper.rb +7 -7
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1fa182ed254be40433e84f2bdef8faf341581e5f
|
4
|
+
data.tar.gz: b795a7e03b4cf94a9d5ff3fd77e04186cde84d5c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
5
|
-
- 2.
|
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/
|
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
|
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
|
-
#
|
54
|
+
# Access the loaded images for the Product.
|
51
55
|
@images = @product.fetch_images
|
52
56
|
```
|
53
57
|
|
data/dev.yml
CHANGED
data/identity_cache.gemspec
CHANGED
@@ -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.
|
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')
|
data/lib/identity_cache.rb
CHANGED
@@ -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.
|
59
|
-
|
60
|
-
base.
|
61
|
-
|
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
|
-
|
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]} =
|
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 &&
|
45
|
-
|
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
|
-
|
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
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
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
|
-
|
267
|
-
|
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)
|
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
|
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
|
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
|
-
|
101
|
-
|
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
|
-
|
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,
|
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
|
-
|
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 &&
|
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 =
|
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 &&
|
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
|
-
|
421
|
-
|
421
|
+
ivar_full_name = :"@#{ivar_name}"
|
422
|
+
assoc = association(association_name)
|
422
423
|
|
423
|
-
|
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 =
|
427
|
-
|
428
|
-
|
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
|
-
|
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
|
487
|
-
|
483
|
+
def readonly_record_copy(record)
|
484
|
+
record = record.clone
|
485
|
+
record.readonly!
|
486
|
+
record
|
487
|
+
end
|
488
488
|
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
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
|
data/performance/cache_runner.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
34
|
+
assert_nil AssociatedRecord.fetch_name_by_id(2)
|
35
35
|
|
36
36
|
assert_queries(0) do
|
37
|
-
|
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) {
|
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) {
|
72
|
-
assert_queries(0) {
|
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
|
-
|
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
|
-
|
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')
|
data/test/fetch_multi_test.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
165
|
+
assert_nil Item.fetch_by_title('bob') # exec_query => nil
|
166
166
|
|
167
|
-
|
168
|
-
|
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
|
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
|
|
data/test/identity_cache_test.rb
CHANGED
@@ -2,14 +2,17 @@ require "test_helper"
|
|
2
2
|
|
3
3
|
class IdentityCacheTest < IdentityCache::TestCase
|
4
4
|
|
5
|
-
class
|
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
|
data/test/index_cache_test.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
47
|
-
|
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
|
-
|
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")
|
data/test/readonly_test.rb
CHANGED
data/test/schema_change_test.rb
CHANGED
@@ -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
|
-
|
18
|
-
def
|
19
|
-
instrument(
|
20
|
-
|
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.
|
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.
|
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.
|
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:
|
265
|
+
version: 2.2.0
|
264
266
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
265
267
|
requirements:
|
266
268
|
- - ">="
|