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