identity_cache 0.4.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +5 -5
  2. data/.github/probots.yml +2 -0
  3. data/.github/workflows/ci.yml +92 -0
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +5 -0
  6. data/CAVEATS.md +25 -0
  7. data/CHANGELOG.md +73 -19
  8. data/Gemfile +5 -1
  9. data/LICENSE +1 -1
  10. data/README.md +49 -27
  11. data/Rakefile +14 -5
  12. data/dev.yml +12 -16
  13. data/gemfiles/Gemfile.latest-release +8 -0
  14. data/gemfiles/Gemfile.min-supported +7 -0
  15. data/gemfiles/Gemfile.rails-edge +7 -0
  16. data/identity_cache.gemspec +29 -10
  17. data/lib/identity_cache.rb +78 -51
  18. data/lib/identity_cache/belongs_to_caching.rb +12 -40
  19. data/lib/identity_cache/cache_fetcher.rb +6 -5
  20. data/lib/identity_cache/cache_hash.rb +2 -2
  21. data/lib/identity_cache/cache_invalidation.rb +4 -11
  22. data/lib/identity_cache/cache_key_generation.rb +17 -65
  23. data/lib/identity_cache/cache_key_loader.rb +128 -0
  24. data/lib/identity_cache/cached.rb +7 -0
  25. data/lib/identity_cache/cached/association.rb +87 -0
  26. data/lib/identity_cache/cached/attribute.rb +123 -0
  27. data/lib/identity_cache/cached/attribute_by_multi.rb +37 -0
  28. data/lib/identity_cache/cached/attribute_by_one.rb +88 -0
  29. data/lib/identity_cache/cached/belongs_to.rb +100 -0
  30. data/lib/identity_cache/cached/embedded_fetching.rb +41 -0
  31. data/lib/identity_cache/cached/prefetcher.rb +61 -0
  32. data/lib/identity_cache/cached/primary_index.rb +96 -0
  33. data/lib/identity_cache/cached/recursive/association.rb +109 -0
  34. data/lib/identity_cache/cached/recursive/has_many.rb +9 -0
  35. data/lib/identity_cache/cached/recursive/has_one.rb +9 -0
  36. data/lib/identity_cache/cached/reference/association.rb +16 -0
  37. data/lib/identity_cache/cached/reference/has_many.rb +105 -0
  38. data/lib/identity_cache/cached/reference/has_one.rb +100 -0
  39. data/lib/identity_cache/configuration_dsl.rb +53 -215
  40. data/lib/identity_cache/encoder.rb +95 -0
  41. data/lib/identity_cache/expiry_hook.rb +36 -0
  42. data/lib/identity_cache/fallback_fetcher.rb +2 -1
  43. data/lib/identity_cache/load_strategy/eager.rb +28 -0
  44. data/lib/identity_cache/load_strategy/lazy.rb +71 -0
  45. data/lib/identity_cache/load_strategy/load_request.rb +20 -0
  46. data/lib/identity_cache/load_strategy/multi_load_request.rb +27 -0
  47. data/lib/identity_cache/mem_cache_store_cas.rb +53 -0
  48. data/lib/identity_cache/memoized_cache_proxy.rb +137 -58
  49. data/lib/identity_cache/parent_model_expiration.rb +46 -11
  50. data/lib/identity_cache/query_api.rb +102 -408
  51. data/lib/identity_cache/railtie.rb +8 -0
  52. data/lib/identity_cache/record_not_found.rb +6 -0
  53. data/lib/identity_cache/should_use_cache.rb +1 -0
  54. data/lib/identity_cache/version.rb +3 -2
  55. data/lib/identity_cache/with_primary_index.rb +136 -0
  56. data/lib/identity_cache/without_primary_index.rb +24 -3
  57. data/performance/cache_runner.rb +25 -73
  58. data/performance/cpu.rb +4 -3
  59. data/performance/externals.rb +4 -3
  60. data/performance/profile.rb +6 -5
  61. data/railgun.yml +16 -0
  62. metadata +60 -73
  63. data/.travis.yml +0 -30
  64. data/Gemfile.rails42 +0 -6
  65. data/Gemfile.rails50 +0 -6
  66. data/test/attribute_cache_test.rb +0 -110
  67. data/test/cache_fetch_includes_test.rb +0 -46
  68. data/test/cache_hash_test.rb +0 -14
  69. data/test/cache_invalidation_test.rb +0 -139
  70. data/test/deeply_nested_associated_record_test.rb +0 -19
  71. data/test/denormalized_has_many_test.rb +0 -211
  72. data/test/denormalized_has_one_test.rb +0 -160
  73. data/test/fetch_multi_test.rb +0 -308
  74. data/test/fetch_test.rb +0 -258
  75. data/test/fixtures/serialized_record.mysql2 +0 -0
  76. data/test/fixtures/serialized_record.postgresql +0 -0
  77. data/test/helpers/active_record_objects.rb +0 -106
  78. data/test/helpers/database_connection.rb +0 -72
  79. data/test/helpers/serialization_format.rb +0 -42
  80. data/test/helpers/update_serialization_format.rb +0 -24
  81. data/test/identity_cache_test.rb +0 -29
  82. data/test/index_cache_test.rb +0 -161
  83. data/test/memoized_attributes_test.rb +0 -49
  84. data/test/memoized_cache_proxy_test.rb +0 -107
  85. data/test/normalized_belongs_to_test.rb +0 -107
  86. data/test/normalized_has_many_test.rb +0 -231
  87. data/test/normalized_has_one_test.rb +0 -9
  88. data/test/prefetch_associations_test.rb +0 -364
  89. data/test/readonly_test.rb +0 -109
  90. data/test/recursive_denormalized_has_many_test.rb +0 -131
  91. data/test/save_test.rb +0 -82
  92. data/test/schema_change_test.rb +0 -112
  93. data/test/serialization_format_change_test.rb +0 -16
  94. 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, :expires_in => IdentityCache::DELETED_TTL.seconds)
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, :unless_exist => true) if IdentityCache.should_fill_cache?
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 <<-NOTICE
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.send(:all_cached_associations).each do |_, data|
15
- CACHE_KEY_NAMES.each do |key|
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}:".freeze
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.denormalized_schema_hash(klass)
11
- schema_string = schema_to_string(klass.columns)
12
- if klass.include?(IdentityCache)
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
- case options[:embed]
16
- when true
17
- schema_string << ",#{name}:(#{denormalized_schema_hash(options[:association_reflection].klass)})"
18
- when :ids
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,7 @@
1
+ # frozen_string_literal: true
2
+ module IdentityCache
3
+ module Cached
4
+ end
5
+
6
+ private_constant :Cached
7
+ 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