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,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module IdentityCache
|
2
3
|
class CacheFetcher
|
3
4
|
attr_accessor :cache_backend
|
@@ -11,7 +12,7 @@ module IdentityCache
|
|
11
12
|
end
|
12
13
|
|
13
14
|
def delete(key)
|
14
|
-
@cache_backend.write(key, IdentityCache::DELETED, :
|
15
|
+
@cache_backend.write(key, IdentityCache::DELETED, expires_in: IdentityCache::DELETED_TTL.seconds)
|
15
16
|
end
|
16
17
|
|
17
18
|
def clear
|
@@ -49,8 +50,8 @@ module IdentityCache
|
|
49
50
|
def cas_multi(keys)
|
50
51
|
result = nil
|
51
52
|
@cache_backend.cas_multi(*keys) do |results|
|
52
|
-
deleted = results.select {|_, v| IdentityCache::DELETED == v }
|
53
|
-
results.reject! {|_, v| IdentityCache::DELETED == v }
|
53
|
+
deleted = results.select { |_, v| IdentityCache::DELETED == v }
|
54
|
+
results.reject! { |_, v| IdentityCache::DELETED == v }
|
54
55
|
|
55
56
|
result = results
|
56
57
|
updates = {}
|
@@ -77,11 +78,11 @@ module IdentityCache
|
|
77
78
|
def add_multi(keys)
|
78
79
|
values = yield keys
|
79
80
|
result = Hash[keys.zip(values)]
|
80
|
-
result.each {|k, v| add(k, v) }
|
81
|
+
result.each { |k, v| add(k, v) }
|
81
82
|
end
|
82
83
|
|
83
84
|
def add(key, value)
|
84
|
-
@cache_backend.write(key, value, :
|
85
|
+
@cache_backend.write(key, value, unless_exist: true) if IdentityCache.should_fill_cache?
|
85
86
|
end
|
86
87
|
end
|
87
88
|
end
|
@@ -1,9 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
# Use CityHash for fast hashing if it is available; use Digest::MD5 otherwise
|
2
3
|
begin
|
3
4
|
require 'cityhash'
|
4
5
|
rescue LoadError
|
5
6
|
unless RUBY_PLATFORM == 'java'
|
6
|
-
warn
|
7
|
+
warn(<<-NOTICE)
|
7
8
|
** Notice: CityHash was not loaded. **
|
8
9
|
|
9
10
|
For optimal performance, use of the cityhash gem is recommended.
|
@@ -19,7 +20,6 @@ end
|
|
19
20
|
|
20
21
|
module IdentityCache
|
21
22
|
module CacheHash
|
22
|
-
|
23
23
|
if defined?(CityHash)
|
24
24
|
|
25
25
|
def memcache_hash(key) #:nodoc:
|
@@ -1,7 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module IdentityCache
|
2
3
|
module CacheInvalidation
|
3
|
-
|
4
|
-
CACHE_KEY_NAMES = [:ids_variable_name, :records_variable_name]
|
4
|
+
CACHE_KEY_NAMES = [:ids_variable_name, :id_variable_name, :records_variable_name]
|
5
5
|
|
6
6
|
def reload(*)
|
7
7
|
clear_cached_associations
|
@@ -11,15 +11,8 @@ module IdentityCache
|
|
11
11
|
private
|
12
12
|
|
13
13
|
def clear_cached_associations
|
14
|
-
self.class.
|
15
|
-
|
16
|
-
if data[key]
|
17
|
-
instance_variable_name = "@#{data[key]}"
|
18
|
-
if instance_variable_defined?(instance_variable_name)
|
19
|
-
remove_instance_variable(instance_variable_name)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
14
|
+
self.class.all_cached_associations.each_value do |association|
|
15
|
+
association.clear(self)
|
23
16
|
end
|
24
17
|
end
|
25
18
|
end
|
@@ -1,88 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module IdentityCache
|
2
3
|
module CacheKeyGeneration
|
3
4
|
extend ActiveSupport::Concern
|
4
|
-
DEFAULT_NAMESPACE = "IDC:#{CACHE_VERSION}:"
|
5
|
+
DEFAULT_NAMESPACE = "IDC:#{CACHE_VERSION}:"
|
5
6
|
|
6
7
|
def self.schema_to_string(columns)
|
7
|
-
columns.sort_by(&:name).map{|c| "#{c.name}:#{c.type}"}.join(',')
|
8
|
+
columns.sort_by(&:name).map { |c| "#{c.name}:#{c.type}" }.join(',')
|
8
9
|
end
|
9
10
|
|
10
|
-
def self.
|
11
|
-
|
12
|
-
|
13
|
-
klass.send(:all_cached_associations).sort.each do |name, options|
|
11
|
+
def self.denormalized_schema_string(klass)
|
12
|
+
schema_to_string(klass.columns).tap do |schema_string|
|
13
|
+
klass.all_cached_associations.sort.each do |name, association|
|
14
14
|
klass.send(:check_association_scope, name)
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
association.validate if association.embedded?
|
16
|
+
case association
|
17
|
+
when Cached::Recursive::Association
|
18
|
+
schema_string << ",#{name}:(#{denormalized_schema_hash(association.reflection.klass)})"
|
19
|
+
when Cached::Reference::HasMany
|
19
20
|
schema_string << ",#{name}:ids"
|
21
|
+
when Cached::Reference::HasOne
|
22
|
+
schema_string << ",#{name}:id"
|
20
23
|
end
|
21
24
|
end
|
22
25
|
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.denormalized_schema_hash(klass)
|
29
|
+
schema_string = denormalized_schema_string(klass)
|
23
30
|
IdentityCache.memcache_hash(schema_string)
|
24
31
|
end
|
25
32
|
|
26
33
|
module ClassMethods
|
27
|
-
def rails_cache_key(id)
|
28
|
-
"#{prefixed_rails_cache_key}#{id}"
|
29
|
-
end
|
30
|
-
|
31
|
-
def rails_cache_key_prefix
|
32
|
-
@rails_cache_key_prefix ||= IdentityCache::CacheKeyGeneration.denormalized_schema_hash(self)
|
33
|
-
end
|
34
|
-
|
35
|
-
def prefixed_rails_cache_key
|
36
|
-
"#{rails_cache_key_namespace}blob:#{base_class.name}:#{rails_cache_key_prefix}:"
|
37
|
-
end
|
38
|
-
|
39
|
-
def rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values, unique)
|
40
|
-
unique_indicator = unique ? '' : 's'
|
41
|
-
"#{rails_cache_key_namespace}" \
|
42
|
-
"attr#{unique_indicator}" \
|
43
|
-
":#{base_class.name}" \
|
44
|
-
":#{attribute}" \
|
45
|
-
":#{rails_cache_string_for_fields_and_values(fields, values)}"
|
46
|
-
end
|
47
|
-
|
48
34
|
def rails_cache_key_namespace
|
49
35
|
ns = IdentityCache.cache_namespace
|
50
36
|
ns.is_a?(Proc) ? ns.call(self) : ns
|
51
37
|
end
|
52
|
-
|
53
|
-
private
|
54
|
-
def rails_cache_string_for_fields_and_values(fields, values)
|
55
|
-
"#{fields.join('/')}:#{IdentityCache.memcache_hash(values.join('/'))}"
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
def primary_cache_index_key # :nodoc:
|
60
|
-
self.class.rails_cache_key(id)
|
61
|
-
end
|
62
|
-
|
63
|
-
def attribute_cache_key_for_attribute_and_current_values(attribute, fields, unique) # :nodoc:
|
64
|
-
self.class.rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, current_values_for_fields(fields), unique)
|
65
|
-
end
|
66
|
-
|
67
|
-
def attribute_cache_key_for_attribute_and_previous_values(attribute, fields, unique) # :nodoc:
|
68
|
-
self.class.rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, old_values_for_fields(fields), unique)
|
69
|
-
end
|
70
|
-
|
71
|
-
def current_values_for_fields(fields) # :nodoc:
|
72
|
-
fields.collect {|field| self.send(field)}
|
73
|
-
end
|
74
|
-
|
75
|
-
def old_values_for_fields(fields) # :nodoc:
|
76
|
-
fields.map do |field|
|
77
|
-
field_string = field.to_s
|
78
|
-
if destroyed? && transaction_changed_attributes.has_key?(field_string)
|
79
|
-
transaction_changed_attributes[field_string]
|
80
|
-
elsif persisted? && transaction_changed_attributes.has_key?(field_string)
|
81
|
-
transaction_changed_attributes[field_string]
|
82
|
-
else
|
83
|
-
self.send(field)
|
84
|
-
end
|
85
|
-
end
|
86
38
|
end
|
87
39
|
end
|
88
40
|
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IdentityCache
|
4
|
+
# A generic cache key loader that supports different types of
|
5
|
+
# cache fetchers, each of which can use their own cache key
|
6
|
+
# format and have their own cache miss resolvers.
|
7
|
+
#
|
8
|
+
# Here is the interface of a cache fetcher in the
|
9
|
+
# [ruby-signature](https://github.com/ruby/ruby-signature)'s
|
10
|
+
# format.
|
11
|
+
#
|
12
|
+
# ```
|
13
|
+
# interface _CacheFetcher[DbKey, DbValue, CacheableValue]
|
14
|
+
# def cache_key: (DbKey) -> String
|
15
|
+
# def cache_encode: (DbValue) -> CacheableValue
|
16
|
+
# def cache_decode: (CacheableValue) -> DbValue
|
17
|
+
# def load_one_from_db: (DbKey) -> DbValue
|
18
|
+
# def load_multi_from_db: (Array[DbKey]) -> Hash[DbKey, DbValue]
|
19
|
+
# end
|
20
|
+
# ```
|
21
|
+
module CacheKeyLoader
|
22
|
+
class << self
|
23
|
+
# Load a single key for a cache fetcher.
|
24
|
+
#
|
25
|
+
# @param cache_fetcher [_CacheFetcher]
|
26
|
+
# @param db_key Reference to what to load from the database.
|
27
|
+
# @return The database value corresponding to the database key.
|
28
|
+
def load(cache_fetcher, db_key)
|
29
|
+
cache_key = cache_fetcher.cache_key(db_key)
|
30
|
+
|
31
|
+
db_value = nil
|
32
|
+
|
33
|
+
cache_value = IdentityCache.fetch(cache_key) do
|
34
|
+
db_value = cache_fetcher.load_one_from_db(db_key)
|
35
|
+
cache_fetcher.cache_encode(db_value)
|
36
|
+
end
|
37
|
+
|
38
|
+
db_value || cache_fetcher.cache_decode(cache_value)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Load multiple keys for a cache fetcher.
|
42
|
+
#
|
43
|
+
# @param cache_fetcher [_CacheFetcher]
|
44
|
+
# @param db_key [Array] Reference to what to load from the database.
|
45
|
+
# @return [Hash] A hash mapping each database key to its corresponding value
|
46
|
+
def load_multi(cache_fetcher, db_keys)
|
47
|
+
load_batch(cache_fetcher => db_keys).fetch(cache_fetcher)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Load multiple keys for multiple cache fetchers
|
51
|
+
def load_batch(cache_fetcher_to_db_keys_hash)
|
52
|
+
cache_key_to_db_key_hash = {}
|
53
|
+
cache_key_to_cache_fetcher_hash = {}
|
54
|
+
|
55
|
+
batch_load_result = {}
|
56
|
+
|
57
|
+
cache_fetcher_to_db_keys_hash.each do |cache_fetcher, db_keys|
|
58
|
+
if db_keys.empty?
|
59
|
+
batch_load_result[cache_fetcher] = {}
|
60
|
+
next
|
61
|
+
end
|
62
|
+
db_keys.each do |db_key|
|
63
|
+
cache_key = cache_fetcher.cache_key(db_key)
|
64
|
+
cache_key_to_db_key_hash[cache_key] = db_key
|
65
|
+
cache_key_to_cache_fetcher_hash[cache_key] = cache_fetcher
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
cache_keys = cache_key_to_db_key_hash.keys
|
70
|
+
cache_result = cache_fetch_multi(cache_keys) do |unresolved_cache_keys|
|
71
|
+
cache_fetcher_to_unresolved_keys_hash = unresolved_cache_keys.group_by do |cache_key|
|
72
|
+
cache_key_to_cache_fetcher_hash.fetch(cache_key)
|
73
|
+
end
|
74
|
+
|
75
|
+
resolve_miss_result = {}
|
76
|
+
|
77
|
+
db_keys_buffer = []
|
78
|
+
cache_fetcher_to_unresolved_keys_hash.each do |cache_fetcher, unresolved_cache_fetcher_keys|
|
79
|
+
batch_load_result[cache_fetcher] = resolve_multi_on_miss(cache_fetcher, unresolved_cache_fetcher_keys,
|
80
|
+
cache_key_to_db_key_hash, resolve_miss_result, db_keys_buffer: db_keys_buffer)
|
81
|
+
end
|
82
|
+
|
83
|
+
resolve_miss_result
|
84
|
+
end
|
85
|
+
|
86
|
+
cache_result.each do |cache_key, cache_value|
|
87
|
+
cache_fetcher = cache_key_to_cache_fetcher_hash.fetch(cache_key)
|
88
|
+
load_result = (batch_load_result[cache_fetcher] ||= {})
|
89
|
+
|
90
|
+
db_key = cache_key_to_db_key_hash.fetch(cache_key)
|
91
|
+
load_result[db_key] ||= cache_fetcher.cache_decode(cache_value)
|
92
|
+
end
|
93
|
+
|
94
|
+
batch_load_result
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def cache_fetch_multi(cache_keys)
|
100
|
+
IdentityCache.fetch_multi(cache_keys) do |unresolved_cache_keys|
|
101
|
+
cache_key_to_cache_value_hash = yield unresolved_cache_keys
|
102
|
+
cache_key_to_cache_value_hash.fetch_values(*unresolved_cache_keys)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def resolve_multi_on_miss(
|
107
|
+
cache_fetcher, unresolved_cache_keys, cache_key_to_db_key_hash, resolve_miss_result,
|
108
|
+
db_keys_buffer: []
|
109
|
+
)
|
110
|
+
db_keys_buffer.clear
|
111
|
+
unresolved_cache_keys.each do |cache_key|
|
112
|
+
db_keys_buffer << cache_key_to_db_key_hash.fetch(cache_key)
|
113
|
+
end
|
114
|
+
|
115
|
+
load_result = cache_fetcher.load_multi_from_db(db_keys_buffer)
|
116
|
+
|
117
|
+
unresolved_cache_keys.each do |cache_key|
|
118
|
+
db_key = cache_key_to_db_key_hash.fetch(cache_key)
|
119
|
+
db_value = load_result[db_key]
|
120
|
+
resolve_miss_result[cache_key] = cache_fetcher.cache_encode(db_value)
|
121
|
+
end
|
122
|
+
|
123
|
+
load_result
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
private_constant :CacheKeyLoader
|
128
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module IdentityCache
|
3
|
+
module Cached
|
4
|
+
class Association # :nodoc:
|
5
|
+
include EmbeddedFetching
|
6
|
+
|
7
|
+
def initialize(name, reflection:)
|
8
|
+
@name = name
|
9
|
+
@reflection = reflection
|
10
|
+
@cached_accessor_name = :"fetch_#{name}"
|
11
|
+
@records_variable_name = :"@cached_#{name}"
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :name, :reflection, :cached_accessor_name, :records_variable_name
|
15
|
+
|
16
|
+
def build
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
|
20
|
+
def read(_record)
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
def write(_record, _value)
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
def clear(_record)
|
29
|
+
raise NotImplementedError
|
30
|
+
end
|
31
|
+
|
32
|
+
def fetch(_records)
|
33
|
+
raise NotImplementedError
|
34
|
+
end
|
35
|
+
|
36
|
+
def fetch_async(_load_strategy, _records)
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
def embedded?
|
41
|
+
embedded_by_reference? || embedded_recursively?
|
42
|
+
end
|
43
|
+
|
44
|
+
def embedded_by_reference?
|
45
|
+
raise NotImplementedError
|
46
|
+
end
|
47
|
+
|
48
|
+
def embedded_recursively?
|
49
|
+
raise NotImplementedError
|
50
|
+
end
|
51
|
+
|
52
|
+
def inverse_name
|
53
|
+
@inverse_name ||= begin
|
54
|
+
reflection.inverse_of&.name ||
|
55
|
+
reflection.active_record.name.underscore
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def validate
|
60
|
+
parent_class = reflection.active_record
|
61
|
+
child_class = reflection.klass
|
62
|
+
|
63
|
+
unless child_class < IdentityCache::WithoutPrimaryIndex
|
64
|
+
if embedded_recursively?
|
65
|
+
raise UnsupportedAssociationError, <<~MSG.squish
|
66
|
+
cached association #{parent_class}\##{reflection.name} requires
|
67
|
+
associated class #{child_class} to include IdentityCache
|
68
|
+
or IdentityCache::WithoutPrimaryIndex
|
69
|
+
MSG
|
70
|
+
else
|
71
|
+
raise UnsupportedAssociationError, <<~MSG.squish
|
72
|
+
cached association #{parent_class}\##{reflection.name} requires
|
73
|
+
associated class #{child_class} to include IdentityCache
|
74
|
+
MSG
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
unless child_class.reflect_on_association(inverse_name)
|
79
|
+
raise InverseAssociationError, <<~MSG
|
80
|
+
Inverse name for association #{parent_class}\##{reflection.name} could not be determined.
|
81
|
+
Use the :inverse_of option on the Active Record association to specify the inverse association name.
|
82
|
+
MSG
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IdentityCache
|
4
|
+
module Cached
|
5
|
+
# @abstract
|
6
|
+
class Attribute
|
7
|
+
attr_reader :model, :alias_name, :key_fields, :unique
|
8
|
+
|
9
|
+
def initialize(model, attribute_or_proc, alias_name, key_fields, unique)
|
10
|
+
@model = model
|
11
|
+
if attribute_or_proc.is_a?(Proc)
|
12
|
+
@attribute_proc = attribute_or_proc
|
13
|
+
else
|
14
|
+
@attribute = attribute_or_proc.to_sym
|
15
|
+
end
|
16
|
+
@alias_name = alias_name.to_sym
|
17
|
+
@key_fields = key_fields.map(&:to_sym)
|
18
|
+
@unique = !!unique
|
19
|
+
end
|
20
|
+
|
21
|
+
def attribute
|
22
|
+
@attribute ||= @attribute_proc.call.to_sym
|
23
|
+
end
|
24
|
+
|
25
|
+
def fetch(db_key)
|
26
|
+
db_key = cast_db_key(db_key)
|
27
|
+
|
28
|
+
if model.should_use_cache?
|
29
|
+
IdentityCache.fetch(cache_key(db_key)) do
|
30
|
+
load_one_from_db(db_key)
|
31
|
+
end
|
32
|
+
else
|
33
|
+
load_one_from_db(db_key)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def expire(record)
|
38
|
+
unless record.send(:was_new_record?)
|
39
|
+
old_key = old_cache_key(record)
|
40
|
+
IdentityCache.cache.delete(old_key)
|
41
|
+
end
|
42
|
+
unless record.destroyed?
|
43
|
+
new_key = new_cache_key(record)
|
44
|
+
if new_key != old_key
|
45
|
+
IdentityCache.cache.delete(new_key)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def cache_key(index_key)
|
51
|
+
values_hash = IdentityCache.memcache_hash(unhashed_values_cache_key_string(index_key))
|
52
|
+
"#{model.rails_cache_key_namespace}#{cache_key_prefix}#{values_hash}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def load_one_from_db(key)
|
56
|
+
query = model.reorder(nil).where(load_from_db_where_conditions(key))
|
57
|
+
query = query.limit(1) if unique
|
58
|
+
results = query.pluck(attribute)
|
59
|
+
unique ? results.first : results
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# @abstract
|
65
|
+
def cast_db_key(_index_key)
|
66
|
+
raise NotImplementedError
|
67
|
+
end
|
68
|
+
|
69
|
+
# @abstract
|
70
|
+
def unhashed_values_cache_key_string(_index_key)
|
71
|
+
raise NotImplementedError
|
72
|
+
end
|
73
|
+
|
74
|
+
# @abstract
|
75
|
+
def load_from_db_where_conditions(_index_key_or_keys)
|
76
|
+
raise NotImplementedError
|
77
|
+
end
|
78
|
+
|
79
|
+
# @abstract
|
80
|
+
def cache_key_from_key_values(_key_values)
|
81
|
+
raise NotImplementedError
|
82
|
+
end
|
83
|
+
|
84
|
+
def field_types
|
85
|
+
@field_types ||= key_fields.map { |field| model.type_for_attribute(field.to_s) }
|
86
|
+
end
|
87
|
+
|
88
|
+
def cache_key_prefix
|
89
|
+
@cache_key_prefix ||= begin
|
90
|
+
unique_indicator = unique ? '' : 's'
|
91
|
+
"attr#{unique_indicator}" \
|
92
|
+
":#{model.base_class.name}" \
|
93
|
+
":#{attribute}" \
|
94
|
+
":#{key_fields.join('/')}:"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def new_cache_key(record)
|
99
|
+
new_key_values = key_fields.map { |field| record.send(field) }
|
100
|
+
cache_key_from_key_values(new_key_values)
|
101
|
+
end
|
102
|
+
|
103
|
+
def old_cache_key(record)
|
104
|
+
old_key_values = key_fields.map do |field|
|
105
|
+
field_string = field.to_s
|
106
|
+
changes = record.transaction_changed_attributes
|
107
|
+
if record.destroyed? && changes.key?(field_string)
|
108
|
+
changes[field_string]
|
109
|
+
elsif record.persisted? && changes.key?(field_string)
|
110
|
+
changes[field_string]
|
111
|
+
else
|
112
|
+
record.send(field)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
cache_key_from_key_values(old_key_values)
|
116
|
+
end
|
117
|
+
|
118
|
+
def fetch_method_suffix
|
119
|
+
"#{alias_name}_by_#{key_fields.join('_and_')}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|