identity_cache 0.5.1 → 1.0.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.
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