identity_cache 1.0.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 +4 -4
- data/.github/workflows/ci.yml +75 -9
- data/.gitignore +1 -0
- data/.rubocop.yml +1 -1
- data/CAVEATS.md +25 -0
- data/CHANGELOG.md +43 -23
- data/Gemfile +2 -2
- data/LICENSE +1 -1
- data/README.md +25 -5
- data/dev.yml +4 -1
- data/gemfiles/Gemfile.latest-release +2 -0
- data/gemfiles/Gemfile.min-supported +7 -0
- data/gemfiles/Gemfile.rails-edge +1 -0
- data/identity_cache.gemspec +6 -3
- data/lib/identity_cache.rb +30 -4
- data/lib/identity_cache/cache_invalidation.rb +1 -1
- data/lib/identity_cache/cache_key_generation.rb +1 -1
- data/lib/identity_cache/cached/belongs_to.rb +21 -14
- data/lib/identity_cache/cached/prefetcher.rb +12 -2
- data/lib/identity_cache/cached/primary_index.rb +0 -1
- data/lib/identity_cache/cached/recursive/association.rb +53 -12
- data/lib/identity_cache/cached/reference/has_many.rb +2 -2
- data/lib/identity_cache/cached/reference/has_one.rb +2 -2
- data/lib/identity_cache/mem_cache_store_cas.rb +53 -0
- data/lib/identity_cache/memoized_cache_proxy.rb +19 -9
- data/lib/identity_cache/parent_model_expiration.rb +3 -2
- data/lib/identity_cache/query_api.rb +26 -66
- data/lib/identity_cache/version.rb +1 -1
- data/performance/cache_runner.rb +0 -42
- data/performance/cpu.rb +1 -1
- metadata +28 -12
- data/.travis.yml +0 -45
- data/gemfiles/Gemfile.rails52 +0 -6
data/lib/identity_cache.rb
CHANGED
@@ -44,6 +44,8 @@ require 'identity_cache/with_primary_index'
|
|
44
44
|
module IdentityCache
|
45
45
|
extend ActiveSupport::Concern
|
46
46
|
|
47
|
+
autoload :MemCacheStoreCAS, 'identity_cache/mem_cache_store_cas'
|
48
|
+
|
47
49
|
include WithPrimaryIndex
|
48
50
|
|
49
51
|
CACHED_NIL = :idc_cached_nil
|
@@ -52,10 +54,15 @@ module IdentityCache
|
|
52
54
|
DELETED_TTL = 1000
|
53
55
|
|
54
56
|
class AlreadyIncludedError < StandardError; end
|
57
|
+
|
55
58
|
class AssociationError < StandardError; end
|
59
|
+
|
56
60
|
class InverseAssociationError < StandardError; end
|
61
|
+
|
57
62
|
class UnsupportedScopeError < StandardError; end
|
63
|
+
|
58
64
|
class UnsupportedAssociationError < StandardError; end
|
65
|
+
|
59
66
|
class DerivedModelError < StandardError; end
|
60
67
|
|
61
68
|
mattr_accessor :cache_namespace
|
@@ -105,8 +112,27 @@ module IdentityCache
|
|
105
112
|
end
|
106
113
|
|
107
114
|
def should_use_cache? # :nodoc:
|
108
|
-
|
109
|
-
|
115
|
+
ActiveRecord::Base.connection_handler.connection_pool_list.none? do |pool|
|
116
|
+
pool.active_connection? &&
|
117
|
+
# Rails wraps each of your tests in a transaction, so that any changes
|
118
|
+
# made to the database during the test can be rolled back afterwards.
|
119
|
+
# These transactions are flagged as "unjoinable", which tries to make
|
120
|
+
# your application behave as if they weren't there. In particular:
|
121
|
+
#
|
122
|
+
# - Opening another transaction during the test creates a savepoint,
|
123
|
+
# which can be rolled back independently of the main transaction.
|
124
|
+
# - When those nested transactions complete, any `after_commit`
|
125
|
+
# callbacks for records modified during the transaction will run,
|
126
|
+
# even though the changes haven't actually been committed yet.
|
127
|
+
#
|
128
|
+
# By ignoring unjoinable transactions, IdentityCache's behaviour
|
129
|
+
# during your test suite will more closely match production.
|
130
|
+
#
|
131
|
+
# When there are no open transactions, `current_transaction` returns a
|
132
|
+
# special `NullTransaction` object that is unjoinable, meaning we will
|
133
|
+
# use the cache.
|
134
|
+
pool.connection.current_transaction.joinable?
|
135
|
+
end
|
110
136
|
end
|
111
137
|
|
112
138
|
# Cache retrieval and miss resolver primitive; given a key it will try to
|
@@ -118,7 +144,7 @@ module IdentityCache
|
|
118
144
|
#
|
119
145
|
def fetch(key)
|
120
146
|
if should_use_cache?
|
121
|
-
unmap_cached_nil_for(cache.fetch(key) { map_cached_nil_for
|
147
|
+
unmap_cached_nil_for(cache.fetch(key) { map_cached_nil_for(yield) })
|
122
148
|
else
|
123
149
|
yield
|
124
150
|
end
|
@@ -144,7 +170,7 @@ module IdentityCache
|
|
144
170
|
result = if should_use_cache?
|
145
171
|
fetch_in_batches(keys.uniq) do |missed_keys|
|
146
172
|
results = yield missed_keys
|
147
|
-
results.map { |e| map_cached_nil_for
|
173
|
+
results.map { |e| map_cached_nil_for(e) }
|
148
174
|
end
|
149
175
|
else
|
150
176
|
results = yield keys
|
@@ -10,7 +10,7 @@ module IdentityCache
|
|
10
10
|
|
11
11
|
def self.denormalized_schema_string(klass)
|
12
12
|
schema_to_string(klass.columns).tap do |schema_string|
|
13
|
-
klass.
|
13
|
+
klass.all_cached_associations.sort.each do |name, association|
|
14
14
|
klass.send(:check_association_scope, name)
|
15
15
|
association.validate if association.embedded?
|
16
16
|
case association
|
@@ -27,34 +27,41 @@ module IdentityCache
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
+
def write(owner_record, associated_record)
|
31
|
+
owner_record.instance_variable_set(records_variable_name, associated_record)
|
32
|
+
end
|
33
|
+
|
30
34
|
def fetch(records)
|
31
35
|
fetch_async(LoadStrategy::Eager, records) { |associated_records| associated_records }
|
32
36
|
end
|
33
37
|
|
34
38
|
def fetch_async(load_strategy, records)
|
35
39
|
if reflection.polymorphic?
|
36
|
-
|
40
|
+
type_fetcher_to_db_ids_hash = {}
|
37
41
|
|
38
42
|
records.each do |owner_record|
|
39
43
|
associated_id = owner_record.send(reflection.foreign_key)
|
40
44
|
next unless associated_id && !owner_record.instance_variable_defined?(records_variable_name)
|
41
|
-
|
45
|
+
foreign_type_fetcher = Object.const_get(
|
42
46
|
owner_record.send(reflection.foreign_type)
|
43
47
|
).cached_model.cached_primary_index
|
44
|
-
|
45
|
-
|
46
|
-
end
|
47
|
-
cache_keys_to_associated_ids[associated_cache_key][associated_id] = owner_record
|
48
|
+
db_ids = type_fetcher_to_db_ids_hash[foreign_type_fetcher] ||= []
|
49
|
+
db_ids << associated_id
|
48
50
|
end
|
49
51
|
|
50
|
-
load_strategy.load_batch(
|
52
|
+
load_strategy.load_batch(type_fetcher_to_db_ids_hash) do |batch_load_result|
|
51
53
|
batch_records = []
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
54
|
+
|
55
|
+
records.each do |owner_record|
|
56
|
+
associated_id = owner_record.send(reflection.foreign_key)
|
57
|
+
next unless associated_id && !owner_record.instance_variable_defined?(records_variable_name)
|
58
|
+
foreign_type_fetcher = Object.const_get(
|
59
|
+
owner_record.send(reflection.foreign_type)
|
60
|
+
).cached_model.cached_primary_index
|
61
|
+
|
62
|
+
associated_record = batch_load_result.fetch(foreign_type_fetcher).fetch(associated_id)
|
63
|
+
batch_records << owner_record
|
64
|
+
write(owner_record, associated_record)
|
58
65
|
end
|
59
66
|
|
60
67
|
yield batch_records
|
@@ -73,7 +80,7 @@ module IdentityCache
|
|
73
80
|
) do |associated_records_by_id|
|
74
81
|
associated_records_by_id.each do |id, associated_record|
|
75
82
|
owner_record = ids_to_owner_record.fetch(id)
|
76
|
-
owner_record
|
83
|
+
write(owner_record, associated_record)
|
77
84
|
end
|
78
85
|
|
79
86
|
yield associated_records_by_id.values.compact
|
@@ -37,14 +37,24 @@ module IdentityCache
|
|
37
37
|
private
|
38
38
|
|
39
39
|
def fetch_association(load_strategy, klass, association, records, &block)
|
40
|
-
unless
|
41
|
-
|
40
|
+
unless klass.should_use_cache?
|
41
|
+
preload_records(records, association)
|
42
42
|
return yield
|
43
43
|
end
|
44
44
|
|
45
45
|
cached_association = klass.cached_association(association)
|
46
46
|
cached_association.fetch_async(load_strategy, records, &block)
|
47
47
|
end
|
48
|
+
|
49
|
+
if ActiveRecord.gem_version < Gem::Version.new("6.2.0.alpha")
|
50
|
+
def preload_records(records, association)
|
51
|
+
ActiveRecord::Associations::Preloader.new.preload(records, association)
|
52
|
+
end
|
53
|
+
else
|
54
|
+
def preload_records(records, association)
|
55
|
+
ActiveRecord::Associations::Preloader.new(records: records, associations: association).call
|
56
|
+
end
|
57
|
+
end
|
48
58
|
end
|
49
59
|
end
|
50
60
|
end
|
@@ -60,7 +60,6 @@ module IdentityCache
|
|
60
60
|
def load_multi_from_db(ids)
|
61
61
|
return {} if ids.empty?
|
62
62
|
|
63
|
-
ids = ids.map { |id| model.connection.type_cast(id, id_column) }
|
64
63
|
records = build_query(ids).to_a
|
65
64
|
model.send(:setup_embedded_associations_on_miss, records)
|
66
65
|
records.index_by(&:id)
|
@@ -11,25 +11,42 @@ module IdentityCache
|
|
11
11
|
attr_reader :dehydrated_variable_name
|
12
12
|
|
13
13
|
def build
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
)
|
21
|
-
end
|
22
|
-
RUBY
|
14
|
+
cached_association = self
|
15
|
+
|
16
|
+
model = reflection.active_record
|
17
|
+
model.define_method(cached_accessor_name) do
|
18
|
+
cached_association.read(self)
|
19
|
+
end
|
23
20
|
|
24
21
|
ParentModelExpiration.add_parent_expiry_hook(self)
|
25
22
|
end
|
26
23
|
|
27
24
|
def read(record)
|
28
|
-
record.
|
25
|
+
assoc = record.association(name)
|
26
|
+
|
27
|
+
if assoc.klass.should_use_cache? && !assoc.loaded? && assoc.target.blank?
|
28
|
+
if record.instance_variable_defined?(records_variable_name)
|
29
|
+
record.instance_variable_get(records_variable_name)
|
30
|
+
elsif record.instance_variable_defined?(dehydrated_variable_name)
|
31
|
+
dehydrated_target = record.instance_variable_get(dehydrated_variable_name)
|
32
|
+
association_target = hydrate_association_target(assoc.klass, dehydrated_target)
|
33
|
+
record.remove_instance_variable(dehydrated_variable_name)
|
34
|
+
set_with_inverse(record, association_target)
|
35
|
+
else
|
36
|
+
assoc.load_target
|
37
|
+
end
|
38
|
+
else
|
39
|
+
assoc.load_target
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def write(record, association_target)
|
44
|
+
record.instance_variable_set(records_variable_name, association_target)
|
29
45
|
end
|
30
46
|
|
31
|
-
def
|
32
|
-
record
|
47
|
+
def set_with_inverse(record, association_target)
|
48
|
+
set_inverse(record, association_target)
|
49
|
+
write(record, association_target)
|
33
50
|
end
|
34
51
|
|
35
52
|
def clear(record)
|
@@ -58,6 +75,30 @@ module IdentityCache
|
|
58
75
|
|
59
76
|
private
|
60
77
|
|
78
|
+
def set_inverse(record, association_target)
|
79
|
+
return if association_target.nil?
|
80
|
+
associated_class = reflection.klass
|
81
|
+
inverse_cached_association = associated_class.cached_belongs_tos[inverse_name]
|
82
|
+
return unless inverse_cached_association
|
83
|
+
|
84
|
+
if association_target.is_a?(Array)
|
85
|
+
association_target.each do |child_record|
|
86
|
+
inverse_cached_association.write(child_record, record)
|
87
|
+
end
|
88
|
+
else
|
89
|
+
inverse_cached_association.write(association_target, record)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def hydrate_association_target(associated_class, dehydrated_value)
|
94
|
+
dehydrated_value = IdentityCache.unmap_cached_nil_for(dehydrated_value)
|
95
|
+
if dehydrated_value.is_a?(Array)
|
96
|
+
dehydrated_value.map { |coder| Encoder.decode(coder, associated_class) }
|
97
|
+
else
|
98
|
+
Encoder.decode(dehydrated_value, associated_class)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
61
102
|
def embedded_fetched?(records)
|
62
103
|
record = records.first
|
63
104
|
super || record.instance_variable_defined?(dehydrated_variable_name)
|
@@ -20,8 +20,8 @@ module IdentityCache
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def #{cached_accessor_name}
|
23
|
-
|
24
|
-
if
|
23
|
+
assoc = association(:#{name})
|
24
|
+
if assoc.klass.should_use_cache? && !assoc.loaded? && assoc.target.blank?
|
25
25
|
#{records_variable_name} ||= #{reflection.class_name}.fetch_multi(#{cached_ids_name})
|
26
26
|
else
|
27
27
|
#{name}.to_a
|
@@ -21,8 +21,8 @@ module IdentityCache
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def #{cached_accessor_name}
|
24
|
-
|
25
|
-
if
|
24
|
+
assoc = association(:#{name})
|
25
|
+
if assoc.klass.should_use_cache? && !assoc.loaded?
|
26
26
|
#{records_variable_name} ||= #{reflection.class_name}.fetch(#{cached_id_name}) if #{cached_id_name}
|
27
27
|
else
|
28
28
|
#{name}
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'dalli/cas/client'
|
3
|
+
|
4
|
+
module IdentityCache
|
5
|
+
module MemCacheStoreCAS
|
6
|
+
def cas(name, options = nil)
|
7
|
+
options = merged_options(options)
|
8
|
+
key = normalize_key(name, options)
|
9
|
+
|
10
|
+
rescue_error_with(false) do
|
11
|
+
instrument(:cas, key, options) do
|
12
|
+
@data.with do |connection|
|
13
|
+
connection.cas(key, options[:expires_in].to_i, options) do |raw_value|
|
14
|
+
entry = deserialize_entry(raw_value)
|
15
|
+
value = yield entry.value
|
16
|
+
entry = ActiveSupport::Cache::Entry.new(value, **options)
|
17
|
+
options[:raw] ? entry.value.to_s : entry
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def cas_multi(*names, **options)
|
25
|
+
return if names.empty?
|
26
|
+
|
27
|
+
options = merged_options(options)
|
28
|
+
keys_to_names = names.each_with_object({}) { |name, hash| hash[normalize_key(name, options)] = name }
|
29
|
+
keys = keys_to_names.keys
|
30
|
+
rescue_error_with(false) do
|
31
|
+
instrument(:cas_multi, keys, options) do
|
32
|
+
raw_values = @data.get_multi_cas(keys)
|
33
|
+
|
34
|
+
values = {}
|
35
|
+
raw_values.each do |key, raw_value|
|
36
|
+
entry = deserialize_entry(raw_value.first)
|
37
|
+
values[keys_to_names[key]] = entry.value unless entry.expired?
|
38
|
+
end
|
39
|
+
|
40
|
+
updates = yield values
|
41
|
+
|
42
|
+
updates.each do |name, value|
|
43
|
+
key = normalize_key(name, options)
|
44
|
+
cas_id = raw_values[key].last
|
45
|
+
entry = ActiveSupport::Cache::Entry.new(value, **options)
|
46
|
+
payload = options[:raw] ? entry.value.to_s : entry
|
47
|
+
@data.replace_cas(key, payload, cas_id, options[:expires_in].to_i, options)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -12,6 +12,16 @@ module IdentityCache
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def cache_backend=(cache_adaptor)
|
15
|
+
if cache_adaptor.class.name == 'ActiveSupport::Cache::MemCacheStore'
|
16
|
+
if cache_adaptor.respond_to?(:cas) || cache_adaptor.respond_to?(:cas_multi)
|
17
|
+
unless cache_adaptor.is_a?(MemCacheStoreCAS)
|
18
|
+
raise "#{cache_adaptor} respond to :cas or :cas_multi, that's unexpected"
|
19
|
+
end
|
20
|
+
else
|
21
|
+
cache_adaptor.extend(MemCacheStoreCAS)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
15
25
|
if cache_adaptor.respond_to?(:cas) && cache_adaptor.respond_to?(:cas_multi)
|
16
26
|
@cache_fetcher = CacheFetcher.new(cache_adaptor)
|
17
27
|
else
|
@@ -185,15 +195,15 @@ module IdentityCache
|
|
185
195
|
end
|
186
196
|
|
187
197
|
def log_multi_result(keys, memo_miss_keys, cache_miss_keys)
|
188
|
-
IdentityCache.logger.debug
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
198
|
+
return unless IdentityCache.logger.debug?
|
199
|
+
|
200
|
+
memoized_keys = keys - memo_miss_keys
|
201
|
+
cache_hit_keys = memo_miss_keys - cache_miss_keys
|
202
|
+
missed_keys = cache_miss_keys
|
203
|
+
|
204
|
+
memoized_keys.each { |k| IdentityCache.logger.debug("[IdentityCache] (memoized) cache hit for #{k} (multi)") }
|
205
|
+
cache_hit_keys.each { |k| IdentityCache.logger.debug("[IdentityCache] (backend) cache hit for #{k} (multi)") }
|
206
|
+
missed_keys.each { |k| IdentityCache.logger.debug("[IdentityCache] cache miss for #{k} (multi)") }
|
197
207
|
end
|
198
208
|
end
|
199
209
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
module IdentityCache
|
3
3
|
module ParentModelExpiration # :nodoc:
|
4
4
|
extend ActiveSupport::Concern
|
5
|
+
include ArTransactionChanges
|
5
6
|
|
6
7
|
class << self
|
7
8
|
def add_parent_expiry_hook(cached_association)
|
@@ -20,6 +21,7 @@ module IdentityCache
|
|
20
21
|
end
|
21
22
|
|
22
23
|
def install_pending_parent_expiry_hooks(model)
|
24
|
+
return if lazy_hooks.empty?
|
23
25
|
name = model.name.demodulize
|
24
26
|
if (hooks = lazy_hooks.delete(name))
|
25
27
|
hooks.each(&:install)
|
@@ -36,11 +38,9 @@ module IdentityCache
|
|
36
38
|
included do
|
37
39
|
class_attribute(:parent_expiration_entries)
|
38
40
|
self.parent_expiration_entries = Hash.new { |hash, key| hash[key] = [] }
|
39
|
-
after_commit(:expire_parent_caches)
|
40
41
|
end
|
41
42
|
|
42
43
|
def expire_parent_caches
|
43
|
-
ParentModelExpiration.install_pending_parent_expiry_hooks(cached_model)
|
44
44
|
parents_to_expire = Set.new
|
45
45
|
add_parents_to_cache_expiry_set(parents_to_expire)
|
46
46
|
parents_to_expire.each do |parent|
|
@@ -49,6 +49,7 @@ module IdentityCache
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def add_parents_to_cache_expiry_set(parents_to_expire)
|
52
|
+
ParentModelExpiration.install_pending_parent_expiry_hooks(cached_model)
|
52
53
|
self.class.parent_expiration_entries.each do |association_name, cached_associations|
|
53
54
|
parents_to_expire_on_changes(parents_to_expire, association_name, cached_associations)
|
54
55
|
end
|
@@ -3,10 +3,6 @@ module IdentityCache
|
|
3
3
|
module QueryAPI
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
|
-
included do |base|
|
7
|
-
base.after_commit(:expire_cache)
|
8
|
-
end
|
9
|
-
|
10
6
|
module ClassMethods
|
11
7
|
# Prefetches cached associations on a collection of records
|
12
8
|
def prefetch_associations(includes, records)
|
@@ -18,6 +14,11 @@ module IdentityCache
|
|
18
14
|
cached_has_manys[name] || cached_has_ones[name] || cached_belongs_tos.fetch(name)
|
19
15
|
end
|
20
16
|
|
17
|
+
# @api private
|
18
|
+
def all_cached_associations # :nodoc:
|
19
|
+
cached_has_manys.merge(cached_has_ones).merge(cached_belongs_tos)
|
20
|
+
end
|
21
|
+
|
21
22
|
private
|
22
23
|
|
23
24
|
def raise_if_scoped
|
@@ -81,7 +82,7 @@ module IdentityCache
|
|
81
82
|
association = record.association(association_name)
|
82
83
|
target = association.target
|
83
84
|
target = readonly_copy(target) if readonly
|
84
|
-
|
85
|
+
cached_association.set_with_inverse(record, target)
|
85
86
|
association.reset
|
86
87
|
# reset inverse associations
|
87
88
|
next unless target && association_reflection.has_inverse?
|
@@ -126,10 +127,6 @@ module IdentityCache
|
|
126
127
|
all_cached_associations.select { |_name, association| association.embedded_recursively? }
|
127
128
|
end
|
128
129
|
|
129
|
-
def all_cached_associations
|
130
|
-
cached_has_manys.merge(cached_has_ones).merge(cached_belongs_tos)
|
131
|
-
end
|
132
|
-
|
133
130
|
def embedded_associations
|
134
131
|
all_cached_associations.select { |_name, association| association.embedded? }
|
135
132
|
end
|
@@ -151,6 +148,26 @@ module IdentityCache
|
|
151
148
|
end
|
152
149
|
end
|
153
150
|
|
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)
|
156
|
+
end
|
157
|
+
|
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
|
169
|
+
end
|
170
|
+
|
154
171
|
# Invalidate the cache data associated with the record.
|
155
172
|
def expire_cache
|
156
173
|
expire_attribute_indexes
|
@@ -165,63 +182,6 @@ module IdentityCache
|
|
165
182
|
|
166
183
|
private
|
167
184
|
|
168
|
-
def fetch_recursively_cached_association(ivar_name, dehydrated_ivar_name, association_name) # :nodoc:
|
169
|
-
assoc = association(association_name)
|
170
|
-
|
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)
|
179
|
-
else
|
180
|
-
assoc.load_target
|
181
|
-
end
|
182
|
-
else
|
183
|
-
assoc.load_target
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
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)
|
193
|
-
end
|
194
|
-
end
|
195
|
-
|
196
|
-
def set_embedded_association(association_name, association_target) #:nodoc:
|
197
|
-
model = self.class
|
198
|
-
cached_association = model.cached_association(association_name)
|
199
|
-
|
200
|
-
set_inverse_of_cached_association(cached_association, association_target)
|
201
|
-
|
202
|
-
instance_variable_set(cached_association.records_variable_name, association_target)
|
203
|
-
end
|
204
|
-
|
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
|
223
|
-
end
|
224
|
-
|
225
185
|
def expire_attribute_indexes # :nodoc:
|
226
186
|
cache_indexes.each do |cached_attribute|
|
227
187
|
cached_attribute.expire(self)
|