identity_cache 0.2.5 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +2 -4
- data/CHANGELOG.md +22 -0
- data/Gemfile +2 -0
- data/Gemfile.rails40 +1 -0
- data/Gemfile.rails41 +1 -0
- data/Gemfile.rails42 +1 -0
- data/README.md +7 -0
- data/identity_cache.gemspec +1 -1
- data/lib/identity_cache.rb +5 -2
- data/lib/identity_cache/belongs_to_caching.rb +13 -5
- data/lib/identity_cache/cache_key_generation.rb +12 -19
- data/lib/identity_cache/configuration_dsl.rb +83 -84
- data/lib/identity_cache/parent_model_expiration.rb +4 -3
- data/lib/identity_cache/query_api.rb +93 -91
- data/lib/identity_cache/version.rb +2 -2
- data/test/attribute_cache_test.rb +42 -63
- data/test/deeply_nested_associated_record_test.rb +1 -0
- data/test/denormalized_has_many_test.rb +18 -0
- data/test/denormalized_has_one_test.rb +15 -5
- data/test/fetch_multi_test.rb +25 -3
- data/test/fetch_test.rb +20 -7
- data/test/fixtures/serialized_record.mysql2 +0 -0
- data/test/fixtures/serialized_record.postgresql +0 -0
- data/test/helpers/active_record_objects.rb +10 -0
- data/test/helpers/database_connection.rb +6 -1
- data/test/helpers/serialization_format.rb +1 -1
- data/test/index_cache_test.rb +50 -25
- data/test/normalized_belongs_to_test.rb +21 -6
- data/test/normalized_has_many_test.rb +44 -0
- data/test/{fetch_multi_with_batched_associations_test.rb → prefetch_normalized_associations_test.rb} +41 -3
- data/test/save_test.rb +14 -14
- data/test/schema_change_test.rb +2 -0
- data/test/test_helper.rb +4 -4
- metadata +11 -10
- data/Gemfile.rails32 +0 -5
- data/test/fixtures/serialized_record +0 -0
@@ -38,9 +38,10 @@ module IdentityCache
|
|
38
38
|
old_parent = nil
|
39
39
|
if transaction_changed_attributes[foreign_key].present?
|
40
40
|
begin
|
41
|
-
if parent_association.
|
42
|
-
|
43
|
-
|
41
|
+
if parent_association.polymorphic?
|
42
|
+
parent_class_name = transaction_changed_attributes[parent_association.foreign_type]
|
43
|
+
parent_class_name ||= read_attribute(parent_association.foreign_type)
|
44
|
+
klass = parent_class_name.try!(:safe_constantize)
|
44
45
|
else
|
45
46
|
klass = parent_association.klass
|
46
47
|
end
|
@@ -4,9 +4,6 @@ module IdentityCache
|
|
4
4
|
|
5
5
|
included do |base|
|
6
6
|
base.after_commit :expire_cache
|
7
|
-
if Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new("4.0.4")
|
8
|
-
base.after_touch :expire_cache
|
9
|
-
end
|
10
7
|
end
|
11
8
|
|
12
9
|
module ClassMethods
|
@@ -19,11 +16,12 @@ module IdentityCache
|
|
19
16
|
|
20
17
|
# Default fetcher added to the model on inclusion, it behaves like
|
21
18
|
# ActiveRecord::Base.where(id: id).first
|
22
|
-
def fetch_by_id(id)
|
23
|
-
|
19
|
+
def fetch_by_id(id, options={})
|
20
|
+
ensure_base_model
|
21
|
+
raise_if_scoped
|
24
22
|
raise NotImplementedError, "fetching needs the primary index enabled" unless primary_cache_index_enabled
|
25
|
-
|
26
|
-
|
23
|
+
return unless id
|
24
|
+
record = if IdentityCache.should_use_cache?
|
27
25
|
require_if_necessary do
|
28
26
|
object = nil
|
29
27
|
coder = IdentityCache.fetch(rails_cache_key(id)){ coder_from_record(object = resolve_cache_miss(id)) }
|
@@ -31,22 +29,25 @@ module IdentityCache
|
|
31
29
|
IdentityCache.logger.error "[IDC id mismatch] fetch_by_id_requested=#{id} fetch_by_id_got=#{object.id} for #{object.inspect[(0..100)]} " if object && object.id != id.to_i
|
32
30
|
object
|
33
31
|
end
|
34
|
-
|
35
32
|
else
|
36
|
-
|
33
|
+
resolve_cache_miss(id)
|
37
34
|
end
|
35
|
+
prefetch_associations(options[:includes], [record]) if record && options[:includes]
|
36
|
+
record
|
38
37
|
end
|
39
38
|
|
40
39
|
# Default fetcher added to the model on inclusion, it behaves like
|
41
40
|
# ActiveRecord::Base.find, will raise ActiveRecord::RecordNotFound exception
|
42
41
|
# if id is not in the cache or the db.
|
43
|
-
def fetch(id)
|
44
|
-
fetch_by_id(id) or raise(ActiveRecord::RecordNotFound, "Couldn't find #{self.name} with ID=#{id}")
|
42
|
+
def fetch(id, options={})
|
43
|
+
fetch_by_id(id, options) or raise(ActiveRecord::RecordNotFound, "Couldn't find #{self.name} with ID=#{id}")
|
45
44
|
end
|
46
45
|
|
47
46
|
# Default fetcher added to the model on inclusion, if behaves like
|
48
47
|
# ActiveRecord::Base.find_all_by_id
|
49
48
|
def fetch_multi(*ids)
|
49
|
+
ensure_base_model
|
50
|
+
raise_if_scoped
|
50
51
|
raise NotImplementedError, "fetching needs the primary index enabled" unless primary_cache_index_enabled
|
51
52
|
options = ids.extract_options!
|
52
53
|
ids.flatten!(1)
|
@@ -59,9 +60,7 @@ module IdentityCache
|
|
59
60
|
coders_by_key = IdentityCache.fetch_multi(cache_keys) do |unresolved_keys|
|
60
61
|
ids = unresolved_keys.map {|key| key_to_id_map[key] }
|
61
62
|
records = find_batch(ids)
|
62
|
-
|
63
|
-
found_records.each{ |record| record.send(:populate_association_caches) }
|
64
|
-
key_to_record_map = found_records.index_by{ |record| rails_cache_key(record.id) }
|
63
|
+
key_to_record_map = records.compact.index_by{ |record| rails_cache_key(record.id) }
|
65
64
|
records.map {|record| coder_from_record(record) }
|
66
65
|
end
|
67
66
|
|
@@ -77,23 +76,19 @@ module IdentityCache
|
|
77
76
|
|
78
77
|
private
|
79
78
|
|
79
|
+
def raise_if_scoped
|
80
|
+
if current_scope
|
81
|
+
raise UnsupportedScopeError, "IdentityCache doesn't support rails scopes"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
80
85
|
def record_from_coder(coder) #:nodoc:
|
81
|
-
if coder
|
82
|
-
|
83
|
-
|
84
|
-
if defined?(ActiveRecord::Type::Serialized)
|
85
|
-
coder[:class].columns.find { |t| t.cast_type.is_a?(ActiveRecord::Type::Serialized) }.nil?
|
86
|
-
else
|
87
|
-
coder[:class].serialized_attributes.empty?
|
88
|
-
end
|
89
|
-
unless empty_serialized_attrs
|
90
|
-
coder = coder.dup
|
91
|
-
coder['attributes'] = coder['attributes'].dup
|
92
|
-
end
|
93
|
-
record.init_with(coder)
|
86
|
+
if coder
|
87
|
+
klass = coder[:class]
|
88
|
+
record = klass.instantiate(coder[:attributes].dup)
|
94
89
|
|
95
90
|
coder[:associations].each {|name, value| set_embedded_association(record, name, value) } if coder.has_key?(:associations)
|
96
|
-
coder[:
|
91
|
+
coder[:association_ids].each {|name, ids| record.instance_variable_set(:"@#{record.class.cached_has_manys[name][:ids_variable_name]}", ids) } if coder.has_key?(:association_ids)
|
97
92
|
record
|
98
93
|
end
|
99
94
|
end
|
@@ -114,10 +109,8 @@ module IdentityCache
|
|
114
109
|
end
|
115
110
|
|
116
111
|
def get_embedded_association(record, association, options) #:nodoc:
|
117
|
-
embedded_variable = record.
|
118
|
-
if
|
119
|
-
nil
|
120
|
-
elsif record.class.reflect_on_association(association).collection?
|
112
|
+
embedded_variable = record.public_send(options.fetch(:cached_accessor_name))
|
113
|
+
if record.class.reflect_on_association(association).collection?
|
121
114
|
embedded_variable.map {|e| coder_from_record(e) }
|
122
115
|
else
|
123
116
|
coder_from_record(embedded_variable)
|
@@ -126,8 +119,10 @@ module IdentityCache
|
|
126
119
|
|
127
120
|
def coder_from_record(record) #:nodoc:
|
128
121
|
unless record.nil?
|
129
|
-
coder = {
|
130
|
-
|
122
|
+
coder = {
|
123
|
+
attributes: record.attributes_before_type_cast,
|
124
|
+
class: record.class,
|
125
|
+
}
|
131
126
|
add_cached_associations_to_coder(record, coder)
|
132
127
|
coder
|
133
128
|
end
|
@@ -142,7 +137,7 @@ module IdentityCache
|
|
142
137
|
end
|
143
138
|
end
|
144
139
|
if (cached_has_manys = klass.cached_has_manys).present?
|
145
|
-
coder[:
|
140
|
+
coder[:association_ids] = cached_has_manys.each_with_object({}) do |(name, options), hash|
|
146
141
|
hash[name] = record.instance_variable_get(:"@#{options[:ids_variable_name]}") unless options[:embed] == true
|
147
142
|
end
|
148
143
|
end
|
@@ -168,9 +163,43 @@ module IdentityCache
|
|
168
163
|
end
|
169
164
|
|
170
165
|
def resolve_cache_miss(id)
|
171
|
-
|
172
|
-
|
173
|
-
|
166
|
+
record = self.includes(cache_fetch_includes).reorder(nil).where(primary_key => id).first
|
167
|
+
preload_id_embedded_associations([record]) if record
|
168
|
+
record
|
169
|
+
end
|
170
|
+
|
171
|
+
def preload_id_embedded_associations(records)
|
172
|
+
return if records.empty?
|
173
|
+
each_id_embedded_association do |options|
|
174
|
+
reflection = options.fetch(:association_reflection)
|
175
|
+
child_model = reflection.klass
|
176
|
+
scope = child_model.all
|
177
|
+
scope = scope.instance_exec(nil, &reflection.scope) if reflection.scope
|
178
|
+
|
179
|
+
pairs = scope.where(reflection.foreign_key => records.map(&:id)).pluck(reflection.foreign_key, reflection.active_record_primary_key)
|
180
|
+
ids_by_parent = Hash.new{ |hash, key| hash[key] = [] }
|
181
|
+
pairs.each do |parent_id, child_id|
|
182
|
+
ids_by_parent[parent_id] << child_id
|
183
|
+
end
|
184
|
+
|
185
|
+
records.each do |parent|
|
186
|
+
child_ids = ids_by_parent[parent.id]
|
187
|
+
parent.instance_variable_set(:"@#{options.fetch(:ids_variable_name)}", child_ids)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
recursively_embedded_associations.each_value do |options|
|
191
|
+
child_model = options.fetch(:association_reflection).klass
|
192
|
+
if child_model.include?(IdentityCache)
|
193
|
+
child_records = records.flat_map(&options.fetch(:cached_accessor_name).to_sym).compact
|
194
|
+
child_model.send(:preload_id_embedded_associations, child_records)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def each_id_embedded_association
|
200
|
+
cached_has_manys.each_value do |options|
|
201
|
+
yield options if options.fetch(:embed) == :ids
|
202
|
+
end
|
174
203
|
end
|
175
204
|
|
176
205
|
def recursively_embedded_associations
|
@@ -212,6 +241,7 @@ module IdentityCache
|
|
212
241
|
@id_column ||= columns.detect {|c| c.name == primary_key}
|
213
242
|
ids = ids.map{ |id| connection.type_cast(id, @id_column) }
|
214
243
|
records = where(primary_key => ids).includes(cache_fetch_includes).to_a
|
244
|
+
preload_id_embedded_associations(records)
|
215
245
|
records_by_id = records.index_by(&:id)
|
216
246
|
ids.map{ |id| records_by_id[id] }
|
217
247
|
end
|
@@ -234,7 +264,7 @@ module IdentityCache
|
|
234
264
|
end
|
235
265
|
|
236
266
|
parent_record_to_child_records = Hash.new { |h, k| h[k] = [] }
|
237
|
-
child_records = details[:
|
267
|
+
child_records = details[:association_reflection].klass.fetch_multi(*ids_to_parent_record.keys)
|
238
268
|
child_records.each do |child_record|
|
239
269
|
parent_record = ids_to_parent_record[child_record.id]
|
240
270
|
parent_record_to_child_records[parent_record] << child_record
|
@@ -251,11 +281,16 @@ module IdentityCache
|
|
251
281
|
if details[:embed] == true
|
252
282
|
raise ArgumentError.new("Embedded belongs_to associations do not support prefetching yet.")
|
253
283
|
else
|
284
|
+
reflection = details[:association_reflection]
|
285
|
+
if reflection.polymorphic?
|
286
|
+
raise ArgumentError.new("Polymorphic belongs_to associations do not support prefetching yet.")
|
287
|
+
end
|
288
|
+
|
254
289
|
ids_to_child_record = records.each_with_object({}) do |child_record, hash|
|
255
|
-
parent_id = child_record.send(
|
290
|
+
parent_id = child_record.send(reflection.foreign_key)
|
256
291
|
hash[parent_id] = child_record if parent_id.present?
|
257
292
|
end
|
258
|
-
parent_records =
|
293
|
+
parent_records = reflection.klass.fetch_multi(ids_to_child_record.keys)
|
259
294
|
parent_records.each do |parent_record|
|
260
295
|
child_record = ids_to_child_record[parent_record.id]
|
261
296
|
child_record.send(details[:prepopulate_method_name], parent_record)
|
@@ -277,8 +312,8 @@ module IdentityCache
|
|
277
312
|
raise ArgumentError.new("Unknown cached association #{association} listed for prefetching")
|
278
313
|
end
|
279
314
|
|
280
|
-
if details && details[:
|
281
|
-
details[:
|
315
|
+
if details && details[:association_reflection].klass.respond_to?(:prefetch_associations, true)
|
316
|
+
details[:association_reflection].klass.send(:prefetch_associations, sub_associations, next_level_records)
|
282
317
|
end
|
283
318
|
end
|
284
319
|
end
|
@@ -306,39 +341,22 @@ module IdentityCache
|
|
306
341
|
|
307
342
|
private
|
308
343
|
|
309
|
-
def populate_association_caches # :nodoc:
|
310
|
-
self.class.send(:embedded_associations).each do |cached_association, options|
|
311
|
-
send(options[:population_method_name])
|
312
|
-
reflection = options[:embed] == true && self.class.reflect_on_association(cached_association)
|
313
|
-
if reflection && reflection.klass.respond_to?(:cached_has_manys)
|
314
|
-
child_objects = Array.wrap(send(options[:cached_accessor_name]))
|
315
|
-
child_objects.each{ |child| child.send(:populate_association_caches) }
|
316
|
-
end
|
317
|
-
end
|
318
|
-
end
|
319
|
-
|
320
344
|
def fetch_recursively_cached_association(ivar_name, association_name) # :nodoc:
|
321
|
-
ivar_full_name = :"@#{ivar_name}"
|
322
345
|
if IdentityCache.should_use_cache?
|
323
|
-
|
324
|
-
|
346
|
+
ivar_full_name = :"@#{ivar_name}"
|
347
|
+
|
348
|
+
unless ivar_value = instance_variable_get(ivar_full_name)
|
349
|
+
ivar_value = IdentityCache.map_cached_nil_for(send(association_name))
|
350
|
+
instance_variable_set(ivar_full_name, ivar_value)
|
351
|
+
end
|
352
|
+
|
353
|
+
assoc = IdentityCache.unmap_cached_nil_for(ivar_value)
|
325
354
|
assoc.is_a?(ActiveRecord::Associations::CollectionAssociation) ? assoc.reader : assoc
|
326
355
|
else
|
327
356
|
send(association_name.to_sym)
|
328
357
|
end
|
329
358
|
end
|
330
359
|
|
331
|
-
def populate_recursively_cached_association(ivar_name, association_name) # :nodoc:
|
332
|
-
ivar_full_name = :"@#{ivar_name}"
|
333
|
-
|
334
|
-
value = instance_variable_get(ivar_full_name)
|
335
|
-
return value unless value.nil?
|
336
|
-
|
337
|
-
loaded_association = send(association_name)
|
338
|
-
|
339
|
-
instance_variable_set(ivar_full_name, IdentityCache.map_cached_nil_for(loaded_association))
|
340
|
-
end
|
341
|
-
|
342
360
|
def expire_primary_index # :nodoc:
|
343
361
|
return unless self.class.primary_cache_index_enabled
|
344
362
|
|
@@ -357,39 +375,23 @@ module IdentityCache
|
|
357
375
|
IdentityCache.cache.delete(primary_cache_index_key)
|
358
376
|
end
|
359
377
|
|
360
|
-
def expire_secondary_indexes # :nodoc:
|
361
|
-
return unless self.class.primary_cache_index_enabled
|
362
|
-
cache_indexes.try(:each) do |fields|
|
363
|
-
if self.destroyed?
|
364
|
-
IdentityCache.cache.delete(secondary_cache_index_key_for_previous_values(fields))
|
365
|
-
else
|
366
|
-
new_cache_index_key = secondary_cache_index_key_for_current_values(fields)
|
367
|
-
IdentityCache.cache.delete(new_cache_index_key)
|
368
|
-
|
369
|
-
if !was_new_record?
|
370
|
-
old_cache_index_key = secondary_cache_index_key_for_previous_values(fields)
|
371
|
-
IdentityCache.cache.delete(old_cache_index_key) unless old_cache_index_key == new_cache_index_key
|
372
|
-
end
|
373
|
-
end
|
374
|
-
end
|
375
|
-
end
|
376
|
-
|
377
378
|
def expire_attribute_indexes # :nodoc:
|
378
|
-
|
379
|
+
cache_indexes.each do |(attribute, fields, unique)|
|
379
380
|
unless was_new_record?
|
380
|
-
old_cache_attribute_key = attribute_cache_key_for_attribute_and_previous_values(attribute, fields)
|
381
|
+
old_cache_attribute_key = attribute_cache_key_for_attribute_and_previous_values(attribute, fields, unique)
|
381
382
|
IdentityCache.cache.delete(old_cache_attribute_key)
|
382
383
|
end
|
383
|
-
|
384
|
-
|
385
|
-
|
384
|
+
unless destroyed?
|
385
|
+
new_cache_attribute_key = attribute_cache_key_for_attribute_and_current_values(attribute, fields, unique)
|
386
|
+
if new_cache_attribute_key != old_cache_attribute_key
|
387
|
+
IdentityCache.cache.delete(new_cache_attribute_key)
|
388
|
+
end
|
386
389
|
end
|
387
390
|
end
|
388
391
|
end
|
389
392
|
|
390
393
|
def expire_cache # :nodoc:
|
391
394
|
expire_primary_index
|
392
|
-
expire_secondary_indexes
|
393
395
|
expire_attribute_indexes
|
394
396
|
true
|
395
397
|
end
|
@@ -9,89 +9,80 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
9
9
|
|
10
10
|
@parent = Item.create!(:title => 'bob')
|
11
11
|
@record = @parent.associated_records.create!(:name => 'foo')
|
12
|
-
@name_attribute_key = "#{NAMESPACE}
|
12
|
+
@name_attribute_key = "#{NAMESPACE}attr:AssociatedRecord:name:id:#{cache_hash(@record.id.to_s)}"
|
13
13
|
IdentityCache.cache.clear
|
14
14
|
end
|
15
15
|
|
16
|
-
def test_attribute_values_are_returned_on_cache_hits
|
17
|
-
IdentityCache.cache.expects(:fetch).with(@name_attribute_key).returns('foo')
|
18
|
-
assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
|
19
|
-
end
|
20
|
-
|
21
16
|
def test_attribute_values_are_fetched_and_returned_on_cache_misses
|
22
17
|
fetch = Spy.on(IdentityCache.cache, :fetch).and_call_through
|
23
|
-
expects_fetch_associated_record_name_by_id(1, returns: 'foo')
|
24
18
|
|
25
|
-
|
19
|
+
assert_queries(1) do
|
20
|
+
assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
|
21
|
+
end
|
26
22
|
assert fetch.has_been_called_with?(@name_attribute_key)
|
27
23
|
end
|
28
24
|
|
29
|
-
def
|
30
|
-
# Cache miss, so
|
31
|
-
fetch = Spy.on(IdentityCache.cache, :fetch).and_call_through
|
32
|
-
|
33
|
-
# Grab the value of the attribute from the DB
|
34
|
-
expects_fetch_associated_record_name_by_id(1, returns: 'foo')
|
35
|
-
|
36
|
-
# And write it back to the cache
|
37
|
-
add = Spy.on(fetcher, :add).and_call_through
|
38
|
-
|
25
|
+
def test_attribute_values_are_returned_on_cache_hits
|
39
26
|
assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
|
40
|
-
|
41
|
-
|
42
|
-
|
27
|
+
|
28
|
+
assert_queries(0) do
|
29
|
+
assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
|
30
|
+
end
|
43
31
|
end
|
44
32
|
|
45
33
|
def test_nil_is_stored_in_the_cache_on_cache_misses
|
46
|
-
|
47
|
-
fetch = Spy.on(IdentityCache.cache, :fetch).and_call_through
|
48
|
-
|
49
|
-
# Grab the value of the attribute from the DB
|
50
|
-
expects_fetch_associated_record_name_by_id(1, returns: nil)
|
51
|
-
|
52
|
-
# And write it back to the cache
|
53
|
-
add = Spy.on(fetcher, :add).and_call_through
|
34
|
+
assert_equal nil, AssociatedRecord.fetch_name_by_id(2)
|
54
35
|
|
55
|
-
|
56
|
-
|
57
|
-
|
36
|
+
assert_queries(0) do
|
37
|
+
assert_equal nil, AssociatedRecord.fetch_name_by_id(2)
|
38
|
+
end
|
58
39
|
end
|
59
40
|
|
60
41
|
def test_cached_attribute_values_are_expired_from_the_cache_when_an_existing_record_is_saved
|
61
|
-
|
62
|
-
|
42
|
+
assert_queries(1) { assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1) }
|
43
|
+
assert_queries(0) { assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1) }
|
44
|
+
|
63
45
|
@record.save!
|
46
|
+
|
47
|
+
assert_queries(1) { assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1) }
|
64
48
|
end
|
65
49
|
|
66
50
|
def test_cached_attribute_values_are_expired_from_the_cache_when_an_existing_record_with_changed_attributes_is_saved
|
67
|
-
|
68
|
-
|
51
|
+
assert_queries(1) { assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1) }
|
52
|
+
assert_queries(0) { assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1) }
|
53
|
+
|
69
54
|
@record.name = 'bar'
|
70
55
|
@record.save!
|
56
|
+
|
57
|
+
assert_queries(1) { assert_equal 'bar', AssociatedRecord.fetch_name_by_id(1) }
|
71
58
|
end
|
72
59
|
|
73
60
|
def test_cached_attribute_values_are_expired_from_the_cache_when_an_existing_record_is_destroyed
|
74
|
-
|
75
|
-
|
61
|
+
assert_queries(1) { assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1) }
|
62
|
+
assert_queries(0) { assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1) }
|
63
|
+
|
76
64
|
@record.destroy
|
65
|
+
|
66
|
+
assert_queries(1) { assert_equal nil, AssociatedRecord.fetch_name_by_id(1) }
|
77
67
|
end
|
78
68
|
|
79
69
|
def test_cached_attribute_values_are_expired_from_the_cache_when_a_new_record_is_saved
|
80
|
-
new_id = 2
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
IdentityCache.cache.expects(:delete).with("#{NAMESPACE}attribute:AssociatedRecord:name:id:#{cache_hash(new_id)}")
|
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) }
|
73
|
+
|
85
74
|
@parent.associated_records.create(:name => 'bar')
|
75
|
+
|
76
|
+
assert_queries(1) { assert_equal 'bar', AssociatedRecord.fetch_name_by_id(new_id) }
|
86
77
|
end
|
87
78
|
|
88
79
|
def test_fetching_by_attribute_delegates_to_block_if_transactions_are_open
|
89
|
-
IdentityCache.cache.expects(:read).
|
90
|
-
|
91
|
-
expects_fetch_associated_record_name_by_id(1, returns: 'foo')
|
80
|
+
IdentityCache.cache.expects(:read).never
|
92
81
|
|
93
82
|
@record.transaction do
|
94
|
-
|
83
|
+
assert_queries(1) do
|
84
|
+
assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
|
85
|
+
end
|
95
86
|
end
|
96
87
|
end
|
97
88
|
|
@@ -101,21 +92,9 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
101
92
|
assert_equal "Jim", AssociatedRecord.fetch_name_by_id(2)
|
102
93
|
end
|
103
94
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
"#{NAMESPACE}blob:AssociatedRecord:#{cache_hash}:#{id}"
|
109
|
-
end
|
110
|
-
|
111
|
-
def quoted_table_column(model, column_name)
|
112
|
-
"#{model.quoted_table_name}.#{model.connection.quote_column_name(column_name)}"
|
113
|
-
end
|
114
|
-
|
115
|
-
def expects_fetch_associated_record_name_by_id(id, options={})
|
116
|
-
result = options[:returns] ? [options[:returns]] : []
|
117
|
-
Item.connection.expects(:exec_query)
|
118
|
-
.with(AssociatedRecord.unscoped.select(quoted_table_column(AssociatedRecord, :name)).where(id: id).limit(1).to_sql, any_parameters)
|
119
|
-
.returns(ActiveRecord::Result.new(['name'], [result]))
|
95
|
+
def test_cache_attribute_on_derived_model_raises
|
96
|
+
assert_raises(IdentityCache::DerivedModelError) do
|
97
|
+
StiRecordTypeA.cache_attribute :name
|
98
|
+
end
|
120
99
|
end
|
121
100
|
end
|