identity_cache 0.5.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +5 -5
  2. data/.github/probots.yml +2 -0
  3. data/.github/workflows/ci.yml +26 -0
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +5 -0
  6. data/.travis.yml +24 -9
  7. data/CHANGELOG.md +21 -0
  8. data/Gemfile +5 -1
  9. data/README.md +28 -26
  10. data/Rakefile +14 -5
  11. data/dev.yml +9 -16
  12. data/gemfiles/Gemfile.latest-release +6 -0
  13. data/gemfiles/Gemfile.rails-edge +6 -0
  14. data/gemfiles/Gemfile.rails52 +6 -0
  15. data/identity_cache.gemspec +26 -10
  16. data/lib/identity_cache.rb +49 -46
  17. data/lib/identity_cache/belongs_to_caching.rb +12 -40
  18. data/lib/identity_cache/cache_fetcher.rb +6 -5
  19. data/lib/identity_cache/cache_hash.rb +2 -2
  20. data/lib/identity_cache/cache_invalidation.rb +4 -11
  21. data/lib/identity_cache/cache_key_generation.rb +17 -65
  22. data/lib/identity_cache/cache_key_loader.rb +128 -0
  23. data/lib/identity_cache/cached.rb +7 -0
  24. data/lib/identity_cache/cached/association.rb +87 -0
  25. data/lib/identity_cache/cached/attribute.rb +123 -0
  26. data/lib/identity_cache/cached/attribute_by_multi.rb +37 -0
  27. data/lib/identity_cache/cached/attribute_by_one.rb +88 -0
  28. data/lib/identity_cache/cached/belongs_to.rb +93 -0
  29. data/lib/identity_cache/cached/embedded_fetching.rb +41 -0
  30. data/lib/identity_cache/cached/prefetcher.rb +51 -0
  31. data/lib/identity_cache/cached/primary_index.rb +97 -0
  32. data/lib/identity_cache/cached/recursive/association.rb +68 -0
  33. data/lib/identity_cache/cached/recursive/has_many.rb +9 -0
  34. data/lib/identity_cache/cached/recursive/has_one.rb +9 -0
  35. data/lib/identity_cache/cached/reference/association.rb +16 -0
  36. data/lib/identity_cache/cached/reference/has_many.rb +105 -0
  37. data/lib/identity_cache/cached/reference/has_one.rb +100 -0
  38. data/lib/identity_cache/configuration_dsl.rb +53 -215
  39. data/lib/identity_cache/encoder.rb +95 -0
  40. data/lib/identity_cache/expiry_hook.rb +36 -0
  41. data/lib/identity_cache/fallback_fetcher.rb +2 -1
  42. data/lib/identity_cache/load_strategy/eager.rb +28 -0
  43. data/lib/identity_cache/load_strategy/lazy.rb +71 -0
  44. data/lib/identity_cache/load_strategy/load_request.rb +20 -0
  45. data/lib/identity_cache/load_strategy/multi_load_request.rb +27 -0
  46. data/lib/identity_cache/memoized_cache_proxy.rb +127 -58
  47. data/lib/identity_cache/parent_model_expiration.rb +45 -11
  48. data/lib/identity_cache/query_api.rb +128 -394
  49. data/lib/identity_cache/railtie.rb +8 -0
  50. data/lib/identity_cache/record_not_found.rb +6 -0
  51. data/lib/identity_cache/should_use_cache.rb +1 -0
  52. data/lib/identity_cache/version.rb +3 -2
  53. data/lib/identity_cache/with_primary_index.rb +136 -0
  54. data/lib/identity_cache/without_primary_index.rb +24 -3
  55. data/performance/cache_runner.rb +28 -34
  56. data/performance/cpu.rb +3 -2
  57. data/performance/externals.rb +4 -3
  58. data/performance/profile.rb +6 -5
  59. data/railgun.yml +16 -0
  60. metadata +44 -73
  61. data/Gemfile.rails42 +0 -6
  62. data/Gemfile.rails50 +0 -6
  63. data/test/attribute_cache_test.rb +0 -110
  64. data/test/cache_fetch_includes_test.rb +0 -46
  65. data/test/cache_hash_test.rb +0 -14
  66. data/test/cache_invalidation_test.rb +0 -139
  67. data/test/deeply_nested_associated_record_test.rb +0 -19
  68. data/test/denormalized_has_many_test.rb +0 -214
  69. data/test/denormalized_has_one_test.rb +0 -160
  70. data/test/fetch_multi_test.rb +0 -308
  71. data/test/fetch_test.rb +0 -258
  72. data/test/fixtures/serialized_record.mysql2 +0 -0
  73. data/test/fixtures/serialized_record.postgresql +0 -0
  74. data/test/helpers/active_record_objects.rb +0 -106
  75. data/test/helpers/database_connection.rb +0 -72
  76. data/test/helpers/serialization_format.rb +0 -51
  77. data/test/helpers/update_serialization_format.rb +0 -27
  78. data/test/identity_cache_test.rb +0 -29
  79. data/test/index_cache_test.rb +0 -161
  80. data/test/memoized_attributes_test.rb +0 -59
  81. data/test/memoized_cache_proxy_test.rb +0 -107
  82. data/test/normalized_belongs_to_test.rb +0 -107
  83. data/test/normalized_has_many_test.rb +0 -231
  84. data/test/normalized_has_one_test.rb +0 -9
  85. data/test/prefetch_associations_test.rb +0 -379
  86. data/test/readonly_test.rb +0 -109
  87. data/test/recursive_denormalized_has_many_test.rb +0 -131
  88. data/test/save_test.rb +0 -82
  89. data/test/schema_change_test.rb +0 -112
  90. data/test/serialization_format_change_test.rb +0 -16
  91. data/test/test_helper.rb +0 -140
@@ -1,114 +1,29 @@
1
+ # frozen_string_literal: true
1
2
  module IdentityCache
2
3
  module QueryAPI
3
4
  extend ActiveSupport::Concern
4
5
 
5
6
  included do |base|
6
- base.after_commit :expire_cache
7
+ base.after_commit(:expire_cache)
7
8
  end
8
9
 
9
10
  module ClassMethods
10
- # Similar to ActiveRecord::Base#exists? will return true if the id can be
11
- # found in the cache or in the DB.
12
- def exists_with_identity_cache?(id)
13
- raise NotImplementedError, "exists_with_identity_cache? needs the primary index enabled" unless primary_cache_index_enabled
14
- !!fetch_by_id(id)
11
+ # Prefetches cached associations on a collection of records
12
+ def prefetch_associations(includes, records)
13
+ Cached::Prefetcher.prefetch(self, includes, records)
15
14
  end
16
15
 
17
- # Default fetcher added to the model on inclusion, it behaves like
18
- # ActiveRecord::Base.where(id: id).first
19
- def fetch_by_id(id, options={})
20
- ensure_base_model
21
- raise_if_scoped
22
- raise NotImplementedError, "fetching needs the primary index enabled" unless primary_cache_index_enabled
23
- return unless id
24
- record = if should_use_cache?
25
- require_if_necessary do
26
- object = nil
27
- coder = IdentityCache.fetch(rails_cache_key(id)){ coder_from_record(object = resolve_cache_miss(id)) }
28
- object ||= record_from_coder(coder)
29
- if object && object.id.to_s != id.to_s
30
- IdentityCache.logger.error "[IDC id mismatch] fetch_by_id_requested=#{id} fetch_by_id_got=#{object.id} for #{object.inspect[(0..100)]}"
31
- end
32
- object
33
- end
34
- else
35
- resolve_cache_miss(id)
36
- end
37
- prefetch_associations(options[:includes], [record]) if record && options[:includes]
38
- record
39
- end
40
-
41
- # Default fetcher added to the model on inclusion, it behaves like
42
- # ActiveRecord::Base.find, will raise ActiveRecord::RecordNotFound exception
43
- # if id is not in the cache or the db.
44
- def fetch(id, options={})
45
- fetch_by_id(id, options) or raise(ActiveRecord::RecordNotFound, "Couldn't find #{self.name} with ID=#{id}")
46
- end
47
-
48
- # Default fetcher added to the model on inclusion, if behaves like
49
- # ActiveRecord::Base.find_all_by_id
50
- def fetch_multi(*ids)
51
- ensure_base_model
52
- raise_if_scoped
53
- raise NotImplementedError, "fetching needs the primary index enabled" unless primary_cache_index_enabled
54
- options = ids.extract_options!
55
- ids.flatten!(1)
56
- records = if should_use_cache?
57
- require_if_necessary do
58
- cache_keys = ids.map {|id| rails_cache_key(id) }
59
- key_to_id_map = Hash[ cache_keys.zip(ids) ]
60
- key_to_record_map = {}
61
-
62
- coders_by_key = IdentityCache.fetch_multi(cache_keys) do |unresolved_keys|
63
- ids = unresolved_keys.map {|key| key_to_id_map[key] }
64
- records = find_batch(ids)
65
- key_to_record_map = records.compact.index_by{ |record| rails_cache_key(record.id) }
66
- records.map {|record| coder_from_record(record) }
67
- end
68
-
69
- cache_keys.map{ |key| key_to_record_map[key] || record_from_coder(coders_by_key[key]) }
70
- end
71
- else
72
- find_batch(ids)
73
- end
74
- records.compact!
75
- prefetch_associations(options[:includes], records) if options[:includes]
76
- records
77
- end
78
-
79
- def prefetch_associations(associations, records)
80
- records = records.to_a
81
- return if records.empty?
82
-
83
- case associations
84
- when nil
85
- # do nothing
86
- when Symbol
87
- prefetch_one_association(associations, records)
88
- when Array
89
- associations.each do |association|
90
- prefetch_associations(association, records)
91
- end
92
- when Hash
93
- associations.each do |association, sub_associations|
94
- next_level_records = prefetch_one_association(association, records)
95
-
96
- if sub_associations.present?
97
- associated_class = reflect_on_association(association).klass
98
- associated_class.prefetch_associations(sub_associations, next_level_records)
99
- end
100
- end
101
- else
102
- raise TypeError, "Invalid associations class #{associations.class}"
103
- end
104
- nil
16
+ # @api private
17
+ def cached_association(name) # :nodoc:
18
+ cached_has_manys[name] || cached_has_ones[name] || cached_belongs_tos.fetch(name)
105
19
  end
106
20
 
107
21
  private
108
22
 
109
23
  def raise_if_scoped
110
24
  if current_scope
111
- raise UnsupportedScopeError, "IdentityCache doesn't support rails scopes"
25
+ IdentityCache.logger.error("#{name} has scope: #{current_scope.to_sql} (#{current_scope.values.keys})")
26
+ raise UnsupportedScopeError, "IdentityCache doesn't support rails scopes (#{name})"
112
27
  end
113
28
  end
114
29
 
@@ -116,157 +31,99 @@ module IdentityCache
116
31
  association_reflection = reflect_on_association(association_name)
117
32
  scope = association_reflection.scope
118
33
  if scope && !association_reflection.klass.all.instance_exec(&scope).joins_values.empty?
119
- raise UnsupportedAssociationError, "caching association #{self}.#{association_name} scoped with a join isn't supported"
34
+ raise UnsupportedAssociationError, <<~MSG.squish
35
+ caching association #{self}.#{association_name}
36
+ scoped with a join isn't supported
37
+ MSG
120
38
  end
121
39
  end
122
40
 
123
- def record_from_coder(coder) #:nodoc:
124
- if coder
125
- klass = coder[:class].constantize
126
- record = klass.instantiate(coder[:attributes].dup)
41
+ def preload_id_embedded_association(records, cached_association)
42
+ reflection = cached_association.reflection
43
+ child_model = reflection.klass
44
+ scope = child_model.all
45
+ scope = scope.where(reflection.type => base_class.name) if reflection.type
46
+ scope = scope.instance_exec(nil, &reflection.scope) if reflection.scope
127
47
 
128
- coder[:associations].each {|name, value| set_embedded_association(record, name, value) } if coder.has_key?(:associations)
129
- 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)
130
- record.readonly! if IdentityCache.fetch_read_only_records
131
- record
48
+ pairs = scope.where(reflection.foreign_key => records.map(&:id)).pluck(
49
+ reflection.foreign_key, reflection.association_primary_key
50
+ )
51
+ ids_by_parent = Hash.new { |hash, key| hash[key] = [] }
52
+ pairs.each do |parent_id, child_id|
53
+ ids_by_parent[parent_id] << child_id
132
54
  end
133
- end
134
-
135
- def set_inverse_of_cached_has_many(record, association_reflection, child_records)
136
- associated_class = association_reflection.klass
137
- return unless associated_class < IdentityCache
138
-
139
- inverse_name = record.class.cached_has_manys.fetch(association_reflection.name).fetch(:inverse_name)
140
- inverse_cached_association = associated_class.cached_belongs_tos[inverse_name]
141
- return unless inverse_cached_association
142
-
143
- prepopulate_method_name = inverse_cached_association.fetch(:prepopulate_method_name)
144
- child_records.each { |child_record| child_record.send(prepopulate_method_name, record) }
145
- end
146
-
147
- def set_embedded_association(record, association_name, coder_or_array) #:nodoc:
148
- value = if IdentityCache.unmap_cached_nil_for(coder_or_array).nil?
149
- nil
150
- elsif (reflection = record.class.reflect_on_association(association_name)).collection?
151
- associated_records = coder_or_array.map {|e| record_from_coder(e) }
152
55
 
153
- set_inverse_of_cached_has_many(record, reflection, associated_records)
154
-
155
- unless IdentityCache.never_set_inverse_association
156
- association = reflection.association_class.new(record, reflection)
157
- association.target = associated_records
158
- association.target.each {|e| association.set_inverse_instance(e) }
56
+ records.each do |parent|
57
+ child_ids = ids_by_parent[parent.id]
58
+ case cached_association
59
+ when Cached::Reference::HasMany
60
+ parent.instance_variable_set(cached_association.ids_variable_name, child_ids)
61
+ when Cached::Reference::HasOne
62
+ parent.instance_variable_set(cached_association.id_variable_name, child_ids.first)
159
63
  end
160
-
161
- associated_records
162
- else
163
- record_from_coder(coder_or_array)
164
- end
165
- variable_name = record.class.send(:recursively_embedded_associations)[association_name][:records_variable_name]
166
- record.instance_variable_set(:"@#{variable_name}", value)
167
- end
168
-
169
- def get_embedded_association(record, association, options) #:nodoc:
170
- embedded_variable = record.public_send(options.fetch(:cached_accessor_name))
171
- if embedded_variable.respond_to?(:to_ary)
172
- embedded_variable.map {|e| coder_from_record(e) }
173
- else
174
- coder_from_record(embedded_variable)
175
64
  end
176
65
  end
177
66
 
178
- def coder_from_record(record) #:nodoc:
179
- unless record.nil?
180
- coder = {
181
- attributes: record.attributes_before_type_cast.dup,
182
- class: record.class.name,
183
- }
184
- add_cached_associations_to_coder(record, coder)
185
- coder
67
+ def setup_embedded_associations_on_miss(records,
68
+ readonly: IdentityCache.fetch_read_only_records && should_use_cache?)
69
+ return if records.empty?
70
+ records.each(&:readonly!) if readonly
71
+ each_id_embedded_association do |cached_association|
72
+ preload_id_embedded_association(records, cached_association)
186
73
  end
187
- end
74
+ recursively_embedded_associations.each_value do |cached_association|
75
+ association_reflection = cached_association.reflection
76
+ association_name = association_reflection.name
188
77
 
189
- def add_cached_associations_to_coder(record, coder)
190
- klass = record.class
191
- if klass.include?(IdentityCache)
192
- if (recursively_embedded_associations = klass.send(:recursively_embedded_associations)).present?
193
- coder[:associations] = recursively_embedded_associations.each_with_object({}) do |(name, options), hash|
194
- hash[name] = IdentityCache.map_cached_nil_for(get_embedded_association(record, name, options))
195
- end
196
- end
197
- if (cached_has_manys = klass.cached_has_manys).present?
198
- coder[:association_ids] = cached_has_manys.each_with_object({}) do |(name, options), hash|
199
- hash[name] = record.instance_variable_get(:"@#{options[:ids_variable_name]}") unless options[:embed] == true
78
+ # Move the loaded records to the cached association instance variable so they
79
+ # behave the same way if they were loaded from the cache
80
+ records.each do |record|
81
+ association = record.association(association_name)
82
+ target = association.target
83
+ target = readonly_copy(target) if readonly
84
+ record.send(:set_embedded_association, association_name, target)
85
+ association.reset
86
+ # reset inverse associations
87
+ next unless target && association_reflection.has_inverse?
88
+ inverse_name = association_reflection.inverse_of.name
89
+ if target.is_a?(Array)
90
+ target.each { |child_record| child_record.association(inverse_name).reset }
91
+ else
92
+ target.association(inverse_name).reset
200
93
  end
201
94
  end
202
- end
203
- end
204
95
 
205
- def require_if_necessary #:nodoc:
206
- # mem_cache_store returns raw value if unmarshal fails
207
- rval = yield
208
- case rval
209
- when String
210
- rval = Marshal.load(rval)
211
- when Array
212
- rval.map!{ |v| v.kind_of?(String) ? Marshal.load(v) : v }
213
- end
214
- rval
215
- rescue ArgumentError => e
216
- if e.message =~ /undefined [\w\/]+ (\w+)/
217
- ok = Kernel.const_get($1) rescue nil
218
- retry if ok
96
+ child_model = association_reflection.klass
97
+ child_records = records.flat_map(&cached_association.cached_accessor_name).compact
98
+ child_model.send(:setup_embedded_associations_on_miss, child_records, readonly: readonly)
219
99
  end
220
- raise
221
100
  end
222
101
 
223
- def resolve_cache_miss(id)
224
- record = self.includes(cache_fetch_includes).reorder(nil).where(primary_key => id).first
225
- if record
226
- preload_id_embedded_associations([record])
227
- record.readonly! if IdentityCache.fetch_read_only_records && should_use_cache?
228
- end
102
+ def readonly_record_copy(record)
103
+ record = record.clone
104
+ record.readonly!
229
105
  record
230
106
  end
231
107
 
232
- def preload_id_embedded_associations(records)
233
- return if records.empty?
234
- each_id_embedded_association do |options|
235
- reflection = options.fetch(:association_reflection)
236
- child_model = reflection.klass
237
- scope = child_model.all
238
- scope = scope.instance_exec(nil, &reflection.scope) if reflection.scope
239
-
240
- pairs = scope.where(reflection.foreign_key => records.map(&:id)).pluck(reflection.foreign_key, reflection.active_record_primary_key)
241
- ids_by_parent = Hash.new{ |hash, key| hash[key] = [] }
242
- pairs.each do |parent_id, child_id|
243
- ids_by_parent[parent_id] << child_id
244
- end
245
-
246
- records.each do |parent|
247
- child_ids = ids_by_parent[parent.id]
248
- parent.instance_variable_set(:"@#{options.fetch(:ids_variable_name)}", child_ids)
249
- end
250
- end
251
- recursively_embedded_associations.each_value do |options|
252
- child_model = options.fetch(:association_reflection).klass
253
- if child_model.include?(IdentityCache)
254
- child_records = records.flat_map(&options.fetch(:cached_accessor_name).to_sym).compact
255
- child_model.send(:preload_id_embedded_associations, child_records)
256
- end
108
+ def readonly_copy(record_or_records)
109
+ if record_or_records.is_a?(Array)
110
+ record_or_records.map { |record| readonly_record_copy(record) }
111
+ elsif record_or_records
112
+ readonly_record_copy(record_or_records)
257
113
  end
258
114
  end
259
115
 
260
116
  def each_id_embedded_association
261
- cached_has_manys.each_value do |options|
262
- yield options if options.fetch(:embed) == :ids
117
+ cached_has_manys.each_value do |association|
118
+ yield association if association.embedded_by_reference?
119
+ end
120
+ cached_has_ones.each_value do |association|
121
+ yield association if association.embedded_by_reference?
263
122
  end
264
123
  end
265
124
 
266
125
  def recursively_embedded_associations
267
- all_cached_associations.select do |cached_association, options|
268
- options[:embed] == true
269
- end
126
+ all_cached_associations.select { |_name, association| association.embedded_recursively? }
270
127
  end
271
128
 
272
129
  def all_cached_associations
@@ -274,13 +131,11 @@ module IdentityCache
274
131
  end
275
132
 
276
133
  def embedded_associations
277
- all_cached_associations.select do |cached_association, options|
278
- options[:embed]
279
- end
134
+ all_cached_associations.select { |_name, association| association.embedded? }
280
135
  end
281
136
 
282
137
  def cache_fetch_includes
283
- associations_for_identity_cache = recursively_embedded_associations.map do |child_association, options|
138
+ associations_for_identity_cache = recursively_embedded_associations.map do |child_association, _options|
284
139
  child_class = reflect_on_association(child_association).try(:klass)
285
140
 
286
141
  child_includes = child_class.send(:cache_fetch_includes)
@@ -294,203 +149,82 @@ module IdentityCache
294
149
 
295
150
  associations_for_identity_cache.compact
296
151
  end
152
+ end
297
153
 
298
- def find_batch(ids)
299
- return [] if ids.empty?
300
-
301
- @id_column ||= columns.detect {|c| c.name == primary_key}
302
- ids = ids.map{ |id| connection.type_cast(id, @id_column) }
303
- records = where(primary_key => ids).includes(cache_fetch_includes).to_a
304
- records.each(&:readonly!) if IdentityCache.fetch_read_only_records && should_use_cache?
305
- preload_id_embedded_associations(records)
306
- records_by_id = records.index_by(&:id)
307
- ids.map{ |id| records_by_id[id] }
308
- end
309
-
310
- def fetch_embedded_associations(records)
311
- associations = embedded_associations
312
- return if associations.empty?
313
-
314
- return unless primary_cache_index_enabled
315
-
316
- cached_records_by_id = fetch_multi(records.map(&:id)).index_by(&:id)
317
-
318
- associations.each_value do |options|
319
- records.each do |record|
320
- next unless cached_record = cached_records_by_id[record.id]
321
- if options[:embed] == :ids
322
- cached_association = cached_record.public_send(options.fetch(:cached_ids_name))
323
- record.instance_variable_set(:"@#{options.fetch(:ids_variable_name)}", cached_association)
324
- else
325
- cached_association = cached_record.public_send(options.fetch(:cached_accessor_name))
326
- record.instance_variable_set(:"@#{options.fetch(:records_variable_name)}", cached_association)
327
- end
328
- end
329
- end
330
- end
331
-
332
- def prefetch_embedded_association(records, association, details)
333
- # Make the same assumption as ActiveRecord::Associations::Preloader, which is
334
- # that all the records have the same associations loaded, so we can just check
335
- # the first record to see if an association is loaded.
336
- first_record = records.first
337
- return if first_record.association(association).loaded?
338
- iv_name_key = details[:embed] == true ? :records_variable_name : :ids_variable_name
339
- return if first_record.instance_variable_defined?(:"@#{details[iv_name_key]}")
340
- fetch_embedded_associations(records)
341
- end
342
-
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
-
349
- case
350
- when details = cached_has_manys[association]
351
- prefetch_embedded_association(records, association, details)
352
- if details[:embed] == true
353
- child_records = records.flat_map(&details[:cached_accessor_name].to_sym)
354
- else
355
- ids_to_parent_record = records.each_with_object({}) do |record, hash|
356
- child_ids = record.send(details[:cached_ids_name])
357
- child_ids.each do |child_id|
358
- hash[child_id] = record
359
- end
360
- end
361
-
362
- parent_record_to_child_records = Hash.new { |h, k| h[k] = [] }
363
- child_records = details[:association_reflection].klass.fetch_multi(*ids_to_parent_record.keys)
364
- child_records.each do |child_record|
365
- parent_record = ids_to_parent_record[child_record.id]
366
- parent_record_to_child_records[parent_record] << child_record
367
- end
368
-
369
- parent_record_to_child_records.each do |parent, children|
370
- parent.send(details[:prepopulate_method_name], children)
371
- end
372
- end
373
-
374
- next_level_records = child_records
375
-
376
- when details = cached_belongs_tos[association]
377
- if details[:embed] == true
378
- raise ArgumentError.new("Embedded belongs_to associations do not support prefetching yet.")
379
- else
380
- reflection = details[:association_reflection]
381
- if reflection.polymorphic?
382
- raise ArgumentError.new("Polymorphic belongs_to associations do not support prefetching yet.")
383
- end
384
-
385
- cached_iv_name = :"@#{details.fetch(:records_variable_name)}"
386
- ids_to_child_record = records.each_with_object({}) do |child_record, hash|
387
- parent_id = child_record.send(reflection.foreign_key)
388
- if parent_id && !child_record.instance_variable_defined?(cached_iv_name)
389
- hash[parent_id] = child_record
390
- end
391
- end
392
- parent_records = reflection.klass.fetch_multi(ids_to_child_record.keys)
393
- parent_records.each do |parent_record|
394
- child_record = ids_to_child_record[parent_record.id]
395
- child_record.send(details[:prepopulate_method_name], parent_record)
396
- end
397
- end
398
-
399
- next_level_records = parent_records
400
-
401
- when details = cached_has_ones[association]
402
- if details[:embed] == true
403
- prefetch_embedded_association(records, association, details)
404
- parent_records = records.map(&details[:cached_accessor_name].to_sym).compact
405
- else
406
- raise ArgumentError.new("Non-embedded has_one associations do not support prefetching yet.")
407
- end
408
-
409
- next_level_records = parent_records
154
+ # Invalidate the cache data associated with the record.
155
+ def expire_cache
156
+ expire_attribute_indexes
157
+ true
158
+ end
410
159
 
411
- else
412
- raise ArgumentError.new("Unknown cached association #{association} listed for prefetching")
413
- end
414
- next_level_records
415
- end
160
+ # @api private
161
+ def was_new_record? # :nodoc:
162
+ pk = self.class.primary_key
163
+ !destroyed? && transaction_changed_attributes.key?(pk) && transaction_changed_attributes[pk].nil?
416
164
  end
417
165
 
418
166
  private
419
167
 
420
- def fetch_recursively_cached_association(ivar_name, association_name) # :nodoc:
421
- ivar_full_name = :"@#{ivar_name}"
168
+ def fetch_recursively_cached_association(ivar_name, dehydrated_ivar_name, association_name) # :nodoc:
422
169
  assoc = association(association_name)
423
170
 
424
- if assoc.klass.should_use_cache?
425
- if instance_variable_defined?(ivar_full_name)
426
- instance_variable_get(ivar_full_name)
171
+ if assoc.klass.should_use_cache? && !assoc.loaded?
172
+ if instance_variable_defined?(ivar_name)
173
+ instance_variable_get(ivar_name)
174
+ elsif instance_variable_defined?(dehydrated_ivar_name)
175
+ associated_records = hydrate_association_target(assoc.klass, instance_variable_get(dehydrated_ivar_name))
176
+ set_embedded_association(association_name, associated_records)
177
+ remove_instance_variable(dehydrated_ivar_name)
178
+ instance_variable_set(ivar_name, associated_records)
427
179
  else
428
- cached_assoc = assoc.load_target
429
- if IdentityCache.fetch_read_only_records
430
- cached_assoc = readonly_copy(cached_assoc)
431
- end
432
- instance_variable_set(ivar_full_name, cached_assoc)
180
+ assoc.load_target
433
181
  end
434
182
  else
435
183
  assoc.load_target
436
184
  end
437
185
  end
438
186
 
439
- def expire_primary_index # :nodoc:
440
- return unless self.class.primary_cache_index_enabled
441
-
442
- IdentityCache.logger.debug do
443
- extra_keys =
444
- if respond_to?(:updated_at)
445
- old_updated_at = old_values_for_fields([:updated_at]).first
446
- "expiring_last_updated_at=#{old_updated_at}"
447
- else
448
- ""
449
- end
450
-
451
- "[IdentityCache] expiring=#{self.class.name} expiring_id=#{id} #{extra_keys}"
187
+ def hydrate_association_target(associated_class, dehydrated_value) # :nodoc:
188
+ dehydrated_value = IdentityCache.unmap_cached_nil_for(dehydrated_value)
189
+ if dehydrated_value.is_a?(Array)
190
+ dehydrated_value.map { |coder| Encoder.decode(coder, associated_class) }
191
+ else
192
+ Encoder.decode(dehydrated_value, associated_class)
452
193
  end
453
-
454
- IdentityCache.cache.delete(primary_cache_index_key)
455
194
  end
456
195
 
457
- def expire_attribute_indexes # :nodoc:
458
- cache_indexes.each do |(attribute, fields, unique)|
459
- unless was_new_record?
460
- old_cache_attribute_key = attribute_cache_key_for_attribute_and_previous_values(attribute, fields, unique)
461
- IdentityCache.cache.delete(old_cache_attribute_key)
462
- end
463
- unless destroyed?
464
- new_cache_attribute_key = attribute_cache_key_for_attribute_and_current_values(attribute, fields, unique)
465
- if new_cache_attribute_key != old_cache_attribute_key
466
- IdentityCache.cache.delete(new_cache_attribute_key)
467
- end
468
- end
469
- end
470
- end
196
+ def set_embedded_association(association_name, association_target) #:nodoc:
197
+ model = self.class
198
+ cached_association = model.cached_association(association_name)
471
199
 
472
- def expire_cache # :nodoc:
473
- expire_primary_index
474
- expire_attribute_indexes
475
- true
476
- end
200
+ set_inverse_of_cached_association(cached_association, association_target)
477
201
 
478
- def was_new_record? # :nodoc:
479
- pk = self.class.primary_key
480
- !destroyed? && transaction_changed_attributes.has_key?(pk) && transaction_changed_attributes[pk].nil?
202
+ instance_variable_set(cached_association.records_variable_name, association_target)
481
203
  end
482
204
 
483
- def readonly_record_copy(record)
484
- record = record.clone
485
- record.readonly!
486
- record
205
+ def set_inverse_of_cached_association(cached_association, association_target)
206
+ return if association_target.nil?
207
+ associated_class = cached_association.reflection.klass
208
+ inverse_name = cached_association.inverse_name
209
+ inverse_cached_association = associated_class.cached_belongs_tos[inverse_name]
210
+ return unless inverse_cached_association
211
+
212
+ if association_target.is_a?(Array)
213
+ association_target.each do |child_record|
214
+ child_record.instance_variable_set(
215
+ inverse_cached_association.records_variable_name, self
216
+ )
217
+ end
218
+ else
219
+ association_target.instance_variable_set(
220
+ inverse_cached_association.records_variable_name, self
221
+ )
222
+ end
487
223
  end
488
224
 
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)
225
+ def expire_attribute_indexes # :nodoc:
226
+ cache_indexes.each do |cached_attribute|
227
+ cached_attribute.expire(self)
494
228
  end
495
229
  end
496
230
  end