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.
- checksums.yaml +5 -5
- data/.github/probots.yml +2 -0
- data/.github/workflows/ci.yml +26 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +5 -0
- data/.travis.yml +24 -9
- data/CHANGELOG.md +21 -0
- data/Gemfile +5 -1
- data/README.md +28 -26
- data/Rakefile +14 -5
- data/dev.yml +9 -16
- data/gemfiles/Gemfile.latest-release +6 -0
- data/gemfiles/Gemfile.rails-edge +6 -0
- data/gemfiles/Gemfile.rails52 +6 -0
- data/identity_cache.gemspec +26 -10
- data/lib/identity_cache.rb +49 -46
- data/lib/identity_cache/belongs_to_caching.rb +12 -40
- data/lib/identity_cache/cache_fetcher.rb +6 -5
- data/lib/identity_cache/cache_hash.rb +2 -2
- data/lib/identity_cache/cache_invalidation.rb +4 -11
- data/lib/identity_cache/cache_key_generation.rb +17 -65
- data/lib/identity_cache/cache_key_loader.rb +128 -0
- data/lib/identity_cache/cached.rb +7 -0
- data/lib/identity_cache/cached/association.rb +87 -0
- data/lib/identity_cache/cached/attribute.rb +123 -0
- data/lib/identity_cache/cached/attribute_by_multi.rb +37 -0
- data/lib/identity_cache/cached/attribute_by_one.rb +88 -0
- data/lib/identity_cache/cached/belongs_to.rb +93 -0
- data/lib/identity_cache/cached/embedded_fetching.rb +41 -0
- data/lib/identity_cache/cached/prefetcher.rb +51 -0
- data/lib/identity_cache/cached/primary_index.rb +97 -0
- data/lib/identity_cache/cached/recursive/association.rb +68 -0
- data/lib/identity_cache/cached/recursive/has_many.rb +9 -0
- data/lib/identity_cache/cached/recursive/has_one.rb +9 -0
- data/lib/identity_cache/cached/reference/association.rb +16 -0
- data/lib/identity_cache/cached/reference/has_many.rb +105 -0
- data/lib/identity_cache/cached/reference/has_one.rb +100 -0
- data/lib/identity_cache/configuration_dsl.rb +53 -215
- data/lib/identity_cache/encoder.rb +95 -0
- data/lib/identity_cache/expiry_hook.rb +36 -0
- data/lib/identity_cache/fallback_fetcher.rb +2 -1
- data/lib/identity_cache/load_strategy/eager.rb +28 -0
- data/lib/identity_cache/load_strategy/lazy.rb +71 -0
- data/lib/identity_cache/load_strategy/load_request.rb +20 -0
- data/lib/identity_cache/load_strategy/multi_load_request.rb +27 -0
- data/lib/identity_cache/memoized_cache_proxy.rb +127 -58
- data/lib/identity_cache/parent_model_expiration.rb +45 -11
- data/lib/identity_cache/query_api.rb +128 -394
- data/lib/identity_cache/railtie.rb +8 -0
- data/lib/identity_cache/record_not_found.rb +6 -0
- data/lib/identity_cache/should_use_cache.rb +1 -0
- data/lib/identity_cache/version.rb +3 -2
- data/lib/identity_cache/with_primary_index.rb +136 -0
- data/lib/identity_cache/without_primary_index.rb +24 -3
- data/performance/cache_runner.rb +28 -34
- data/performance/cpu.rb +3 -2
- data/performance/externals.rb +4 -3
- data/performance/profile.rb +6 -5
- data/railgun.yml +16 -0
- metadata +44 -73
- data/Gemfile.rails42 +0 -6
- data/Gemfile.rails50 +0 -6
- data/test/attribute_cache_test.rb +0 -110
- data/test/cache_fetch_includes_test.rb +0 -46
- data/test/cache_hash_test.rb +0 -14
- data/test/cache_invalidation_test.rb +0 -139
- data/test/deeply_nested_associated_record_test.rb +0 -19
- data/test/denormalized_has_many_test.rb +0 -214
- data/test/denormalized_has_one_test.rb +0 -160
- data/test/fetch_multi_test.rb +0 -308
- data/test/fetch_test.rb +0 -258
- data/test/fixtures/serialized_record.mysql2 +0 -0
- data/test/fixtures/serialized_record.postgresql +0 -0
- data/test/helpers/active_record_objects.rb +0 -106
- data/test/helpers/database_connection.rb +0 -72
- data/test/helpers/serialization_format.rb +0 -51
- data/test/helpers/update_serialization_format.rb +0 -27
- data/test/identity_cache_test.rb +0 -29
- data/test/index_cache_test.rb +0 -161
- data/test/memoized_attributes_test.rb +0 -59
- data/test/memoized_cache_proxy_test.rb +0 -107
- data/test/normalized_belongs_to_test.rb +0 -107
- data/test/normalized_has_many_test.rb +0 -231
- data/test/normalized_has_one_test.rb +0 -9
- data/test/prefetch_associations_test.rb +0 -379
- data/test/readonly_test.rb +0 -109
- data/test/recursive_denormalized_has_many_test.rb +0 -131
- data/test/save_test.rb +0 -82
- data/test/schema_change_test.rb +0 -112
- data/test/serialization_format_change_test.rb +0 -16
- 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
|
7
|
+
base.after_commit(:expire_cache)
|
7
8
|
end
|
8
9
|
|
9
10
|
module ClassMethods
|
10
|
-
#
|
11
|
-
|
12
|
-
|
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
|
-
#
|
18
|
-
#
|
19
|
-
|
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
|
-
|
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,
|
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
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
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
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
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
|
-
|
74
|
+
recursively_embedded_associations.each_value do |cached_association|
|
75
|
+
association_reflection = cached_association.reflection
|
76
|
+
association_name = association_reflection.name
|
188
77
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
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
|
-
|
206
|
-
|
207
|
-
|
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
|
224
|
-
record =
|
225
|
-
|
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
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
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 |
|
262
|
-
yield
|
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
|
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
|
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,
|
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
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
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
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
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?(
|
426
|
-
instance_variable_get(
|
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
|
-
|
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
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
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
|
458
|
-
|
459
|
-
|
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
|
-
|
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
|
-
|
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
|
484
|
-
|
485
|
-
|
486
|
-
|
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
|
490
|
-
|
491
|
-
|
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
|