identity_cache 0.2.5 → 0.3.0
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 +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
|