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.
- 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
|