identity_cache 0.4.1 → 1.1.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 +92 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +5 -0
- data/CAVEATS.md +25 -0
- data/CHANGELOG.md +73 -19
- data/Gemfile +5 -1
- data/LICENSE +1 -1
- data/README.md +49 -27
- data/Rakefile +14 -5
- data/dev.yml +12 -16
- data/gemfiles/Gemfile.latest-release +8 -0
- data/gemfiles/Gemfile.min-supported +7 -0
- data/gemfiles/Gemfile.rails-edge +7 -0
- data/identity_cache.gemspec +29 -10
- data/lib/identity_cache.rb +78 -51
- 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 +100 -0
- data/lib/identity_cache/cached/embedded_fetching.rb +41 -0
- data/lib/identity_cache/cached/prefetcher.rb +61 -0
- data/lib/identity_cache/cached/primary_index.rb +96 -0
- data/lib/identity_cache/cached/recursive/association.rb +109 -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/mem_cache_store_cas.rb +53 -0
- data/lib/identity_cache/memoized_cache_proxy.rb +137 -58
- data/lib/identity_cache/parent_model_expiration.rb +46 -11
- data/lib/identity_cache/query_api.rb +102 -408
- 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 +25 -73
- data/performance/cpu.rb +4 -3
- data/performance/externals.rb +4 -3
- data/performance/profile.rb +6 -5
- data/railgun.yml +16 -0
- metadata +60 -73
- data/.travis.yml +0 -30
- 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 -211
- 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 -42
- data/test/helpers/update_serialization_format.rb +0 -24
- data/test/identity_cache_test.rb +0 -29
- data/test/index_cache_test.rb +0 -161
- data/test/memoized_attributes_test.rb +0 -49
- 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 -364
- 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,30 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module IdentityCache
|
2
3
|
module ParentModelExpiration # :nodoc:
|
3
4
|
extend ActiveSupport::Concern
|
5
|
+
include ArTransactionChanges
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
7
|
+
class << self
|
8
|
+
def add_parent_expiry_hook(cached_association)
|
9
|
+
name = cached_association.reflection.class_name.demodulize
|
10
|
+
lazy_hooks[name] << ExpiryHook.new(cached_association)
|
11
|
+
end
|
12
|
+
|
13
|
+
def install_all_pending_parent_expiry_hooks
|
14
|
+
until lazy_hooks.empty?
|
15
|
+
lazy_hooks.keys.each do |name|
|
16
|
+
if (hooks = lazy_hooks.delete(name))
|
17
|
+
hooks.each(&:install)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def install_pending_parent_expiry_hooks(model)
|
24
|
+
return if lazy_hooks.empty?
|
25
|
+
name = model.name.demodulize
|
26
|
+
if (hooks = lazy_hooks.delete(name))
|
27
|
+
hooks.each(&:install)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def lazy_hooks
|
34
|
+
@lazy_hooks ||= Hash.new { |hash, key| hash[key] = [] }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
included do
|
39
|
+
class_attribute(:parent_expiration_entries)
|
40
|
+
self.parent_expiration_entries = Hash.new { |hash, key| hash[key] = [] }
|
8
41
|
end
|
9
42
|
|
10
43
|
def expire_parent_caches
|
11
|
-
parents_to_expire =
|
44
|
+
parents_to_expire = Set.new
|
12
45
|
add_parents_to_cache_expiry_set(parents_to_expire)
|
13
|
-
parents_to_expire.
|
14
|
-
parent.
|
46
|
+
parents_to_expire.each do |parent|
|
47
|
+
parent.expire_primary_index if parent.class.primary_cache_index_enabled
|
15
48
|
end
|
16
49
|
end
|
17
50
|
|
18
51
|
def add_parents_to_cache_expiry_set(parents_to_expire)
|
52
|
+
ParentModelExpiration.install_pending_parent_expiry_hooks(cached_model)
|
19
53
|
self.class.parent_expiration_entries.each do |association_name, cached_associations|
|
20
54
|
parents_to_expire_on_changes(parents_to_expire, association_name, cached_associations)
|
21
55
|
end
|
22
56
|
end
|
23
57
|
|
24
58
|
def add_record_to_cache_expiry_set(parents_to_expire, record)
|
25
|
-
|
26
|
-
unless parents_to_expire[key]
|
27
|
-
parents_to_expire[key] = record
|
59
|
+
if parents_to_expire.add?(record)
|
28
60
|
record.add_parents_to_cache_expiry_set(parents_to_expire)
|
29
61
|
end
|
30
62
|
end
|
@@ -52,11 +84,12 @@ module IdentityCache
|
|
52
84
|
end
|
53
85
|
|
54
86
|
cached_associations.each do |parent_class, only_on_foreign_key_change|
|
55
|
-
if new_parent
|
87
|
+
if new_parent&.is_a?(parent_class) &&
|
88
|
+
should_expire_identity_cache_parent?(foreign_key, only_on_foreign_key_change)
|
56
89
|
add_record_to_cache_expiry_set(parents_to_expire, new_parent)
|
57
90
|
end
|
58
91
|
|
59
|
-
if old_parent
|
92
|
+
if old_parent&.is_a?(parent_class)
|
60
93
|
add_record_to_cache_expiry_set(parents_to_expire, old_parent)
|
61
94
|
end
|
62
95
|
end
|
@@ -70,4 +103,6 @@ module IdentityCache
|
|
70
103
|
end
|
71
104
|
end
|
72
105
|
end
|
106
|
+
|
107
|
+
private_constant :ParentModelExpiration
|
73
108
|
end
|
@@ -1,114 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module IdentityCache
|
2
3
|
module QueryAPI
|
3
4
|
extend ActiveSupport::Concern
|
4
5
|
|
5
|
-
included do |base|
|
6
|
-
base.after_commit :expire_cache
|
7
|
-
end
|
8
|
-
|
9
6
|
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)
|
15
|
-
end
|
16
|
-
|
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}")
|
7
|
+
# Prefetches cached associations on a collection of records
|
8
|
+
def prefetch_associations(includes, records)
|
9
|
+
Cached::Prefetcher.prefetch(self, includes, records)
|
46
10
|
end
|
47
11
|
|
48
|
-
#
|
49
|
-
#
|
50
|
-
|
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
|
12
|
+
# @api private
|
13
|
+
def cached_association(name) # :nodoc:
|
14
|
+
cached_has_manys[name] || cached_has_ones[name] || cached_belongs_tos.fetch(name)
|
77
15
|
end
|
78
16
|
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
17
|
+
# @api private
|
18
|
+
def all_cached_associations # :nodoc:
|
19
|
+
cached_has_manys.merge(cached_has_ones).merge(cached_belongs_tos)
|
105
20
|
end
|
106
21
|
|
107
22
|
private
|
108
23
|
|
109
24
|
def raise_if_scoped
|
110
25
|
if current_scope
|
111
|
-
|
26
|
+
IdentityCache.logger.error("#{name} has scope: #{current_scope.to_sql} (#{current_scope.values.keys})")
|
27
|
+
raise UnsupportedScopeError, "IdentityCache doesn't support rails scopes (#{name})"
|
112
28
|
end
|
113
29
|
end
|
114
30
|
|
@@ -116,171 +32,107 @@ module IdentityCache
|
|
116
32
|
association_reflection = reflect_on_association(association_name)
|
117
33
|
scope = association_reflection.scope
|
118
34
|
if scope && !association_reflection.klass.all.instance_exec(&scope).joins_values.empty?
|
119
|
-
raise UnsupportedAssociationError,
|
35
|
+
raise UnsupportedAssociationError, <<~MSG.squish
|
36
|
+
caching association #{self}.#{association_name}
|
37
|
+
scoped with a join isn't supported
|
38
|
+
MSG
|
120
39
|
end
|
121
40
|
end
|
122
41
|
|
123
|
-
def
|
124
|
-
|
125
|
-
|
126
|
-
|
42
|
+
def preload_id_embedded_association(records, cached_association)
|
43
|
+
reflection = cached_association.reflection
|
44
|
+
child_model = reflection.klass
|
45
|
+
scope = child_model.all
|
46
|
+
scope = scope.where(reflection.type => base_class.name) if reflection.type
|
47
|
+
scope = scope.instance_exec(nil, &reflection.scope) if reflection.scope
|
127
48
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
49
|
+
pairs = scope.where(reflection.foreign_key => records.map(&:id)).pluck(
|
50
|
+
reflection.foreign_key, reflection.association_primary_key
|
51
|
+
)
|
52
|
+
ids_by_parent = Hash.new { |hash, key| hash[key] = [] }
|
53
|
+
pairs.each do |parent_id, child_id|
|
54
|
+
ids_by_parent[parent_id] << child_id
|
132
55
|
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
|
-
|
153
|
-
set_inverse_of_cached_has_many(record, reflection, associated_records)
|
154
56
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
57
|
+
records.each do |parent|
|
58
|
+
child_ids = ids_by_parent[parent.id]
|
59
|
+
case cached_association
|
60
|
+
when Cached::Reference::HasMany
|
61
|
+
parent.instance_variable_set(cached_association.ids_variable_name, child_ids)
|
62
|
+
when Cached::Reference::HasOne
|
63
|
+
parent.instance_variable_set(cached_association.id_variable_name, child_ids.first)
|
159
64
|
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
65
|
end
|
176
66
|
end
|
177
67
|
|
178
|
-
def
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
add_cached_associations_to_coder(record, coder)
|
185
|
-
coder
|
68
|
+
def setup_embedded_associations_on_miss(records,
|
69
|
+
readonly: IdentityCache.fetch_read_only_records && should_use_cache?)
|
70
|
+
return if records.empty?
|
71
|
+
records.each(&:readonly!) if readonly
|
72
|
+
each_id_embedded_association do |cached_association|
|
73
|
+
preload_id_embedded_association(records, cached_association)
|
186
74
|
end
|
187
|
-
|
75
|
+
recursively_embedded_associations.each_value do |cached_association|
|
76
|
+
association_reflection = cached_association.reflection
|
77
|
+
association_name = association_reflection.name
|
188
78
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
79
|
+
# Move the loaded records to the cached association instance variable so they
|
80
|
+
# behave the same way if they were loaded from the cache
|
81
|
+
records.each do |record|
|
82
|
+
association = record.association(association_name)
|
83
|
+
target = association.target
|
84
|
+
target = readonly_copy(target) if readonly
|
85
|
+
cached_association.set_with_inverse(record, target)
|
86
|
+
association.reset
|
87
|
+
# reset inverse associations
|
88
|
+
next unless target && association_reflection.has_inverse?
|
89
|
+
inverse_name = association_reflection.inverse_of.name
|
90
|
+
if target.is_a?(Array)
|
91
|
+
target.each { |child_record| child_record.association(inverse_name).reset }
|
92
|
+
else
|
93
|
+
target.association(inverse_name).reset
|
200
94
|
end
|
201
95
|
end
|
202
|
-
end
|
203
|
-
end
|
204
96
|
|
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 }
|
97
|
+
child_model = association_reflection.klass
|
98
|
+
child_records = records.flat_map(&cached_association.cached_accessor_name).compact
|
99
|
+
child_model.send(:setup_embedded_associations_on_miss, child_records, readonly: readonly)
|
213
100
|
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
|
219
|
-
end
|
220
|
-
raise
|
221
101
|
end
|
222
102
|
|
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
|
103
|
+
def readonly_record_copy(record)
|
104
|
+
record = record.clone
|
105
|
+
record.readonly!
|
229
106
|
record
|
230
107
|
end
|
231
108
|
|
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
|
109
|
+
def readonly_copy(record_or_records)
|
110
|
+
if record_or_records.is_a?(Array)
|
111
|
+
record_or_records.map { |record| readonly_record_copy(record) }
|
112
|
+
elsif record_or_records
|
113
|
+
readonly_record_copy(record_or_records)
|
257
114
|
end
|
258
115
|
end
|
259
116
|
|
260
117
|
def each_id_embedded_association
|
261
|
-
cached_has_manys.each_value do |
|
262
|
-
yield
|
118
|
+
cached_has_manys.each_value do |association|
|
119
|
+
yield association if association.embedded_by_reference?
|
263
120
|
end
|
264
|
-
|
265
|
-
|
266
|
-
def recursively_embedded_associations
|
267
|
-
all_cached_associations.select do |cached_association, options|
|
268
|
-
options[:embed] == true
|
121
|
+
cached_has_ones.each_value do |association|
|
122
|
+
yield association if association.embedded_by_reference?
|
269
123
|
end
|
270
124
|
end
|
271
125
|
|
272
|
-
def
|
273
|
-
|
126
|
+
def recursively_embedded_associations
|
127
|
+
all_cached_associations.select { |_name, association| association.embedded_recursively? }
|
274
128
|
end
|
275
129
|
|
276
130
|
def embedded_associations
|
277
|
-
all_cached_associations.select
|
278
|
-
options[:embed]
|
279
|
-
end
|
131
|
+
all_cached_associations.select { |_name, association| association.embedded? }
|
280
132
|
end
|
281
133
|
|
282
134
|
def cache_fetch_includes
|
283
|
-
associations_for_identity_cache = recursively_embedded_associations.map do |child_association,
|
135
|
+
associations_for_identity_cache = recursively_embedded_associations.map do |child_association, _options|
|
284
136
|
child_class = reflect_on_association(child_association).try(:klass)
|
285
137
|
|
286
138
|
child_includes = child_class.send(:cache_fetch_includes)
|
@@ -294,203 +146,45 @@ module IdentityCache
|
|
294
146
|
|
295
147
|
associations_for_identity_cache.compact
|
296
148
|
end
|
297
|
-
|
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)
|
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
|
410
|
-
|
411
|
-
else
|
412
|
-
raise ArgumentError.new("Unknown cached association #{association} listed for prefetching")
|
413
|
-
end
|
414
|
-
next_level_records
|
415
|
-
end
|
416
|
-
end
|
417
|
-
|
418
|
-
private
|
419
|
-
|
420
|
-
def fetch_recursively_cached_association(ivar_name, association_name) # :nodoc:
|
421
|
-
ivar_full_name = :"@#{ivar_name}"
|
422
|
-
assoc = association(association_name)
|
423
|
-
|
424
|
-
if assoc.klass.should_use_cache?
|
425
|
-
if instance_variable_defined?(ivar_full_name)
|
426
|
-
instance_variable_get(ivar_full_name)
|
427
|
-
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)
|
433
|
-
end
|
434
|
-
else
|
435
|
-
assoc.load_target
|
436
|
-
end
|
437
149
|
end
|
438
150
|
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
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}"
|
452
|
-
end
|
453
|
-
|
454
|
-
IdentityCache.cache.delete(primary_cache_index_key)
|
151
|
+
no_op_callback = proc {}
|
152
|
+
included do |base|
|
153
|
+
# Make sure there is at least once after_commit callback so that _run_commit_callbacks
|
154
|
+
# is called, which is overridden to do an early after_commit callback
|
155
|
+
base.after_commit(&no_op_callback)
|
455
156
|
end
|
456
157
|
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
end
|
469
|
-
end
|
158
|
+
# Override the method that is used to call after_commit callbacks so that we can
|
159
|
+
# expire the caches before other after_commit callbacks. This way we can avoid stale
|
160
|
+
# cache reads that happen from the ordering of callbacks. For example, if an after_commit
|
161
|
+
# callback enqueues a background job, then we don't want it to be possible for the
|
162
|
+
# background job to run and load data from the cache before it is invalidated.
|
163
|
+
def _run_commit_callbacks
|
164
|
+
if destroyed? || transaction_changed_attributes.present?
|
165
|
+
expire_cache
|
166
|
+
expire_parent_caches
|
167
|
+
end
|
168
|
+
super
|
470
169
|
end
|
471
170
|
|
472
|
-
|
473
|
-
|
171
|
+
# Invalidate the cache data associated with the record.
|
172
|
+
def expire_cache
|
474
173
|
expire_attribute_indexes
|
475
174
|
true
|
476
175
|
end
|
477
176
|
|
177
|
+
# @api private
|
478
178
|
def was_new_record? # :nodoc:
|
479
179
|
pk = self.class.primary_key
|
480
|
-
!destroyed? && transaction_changed_attributes.
|
180
|
+
!destroyed? && transaction_changed_attributes.key?(pk) && transaction_changed_attributes[pk].nil?
|
481
181
|
end
|
482
182
|
|
483
|
-
|
484
|
-
record = record.clone
|
485
|
-
record.readonly!
|
486
|
-
record
|
487
|
-
end
|
183
|
+
private
|
488
184
|
|
489
|
-
def
|
490
|
-
|
491
|
-
|
492
|
-
elsif record_or_records
|
493
|
-
readonly_record_copy(record_or_records)
|
185
|
+
def expire_attribute_indexes # :nodoc:
|
186
|
+
cache_indexes.each do |cached_attribute|
|
187
|
+
cached_attribute.expire(self)
|
494
188
|
end
|
495
189
|
end
|
496
190
|
end
|