identity_cache 1.2.0 → 1.6.3

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +8 -7
  3. data/.gitignore +1 -1
  4. data/.rubocop.yml +0 -1
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +84 -2
  7. data/Gemfile +4 -4
  8. data/Gemfile.lock +118 -0
  9. data/README.md +28 -5
  10. data/Rakefile +98 -6
  11. data/dev.yml +17 -17
  12. data/gemfiles/Gemfile.latest-release +1 -0
  13. data/gemfiles/Gemfile.min-supported +5 -5
  14. data/gemfiles/Gemfile.rails-edge +1 -0
  15. data/identity_cache.gemspec +3 -3
  16. data/lib/identity_cache/belongs_to_caching.rb +1 -1
  17. data/lib/identity_cache/cache_fetcher.rb +12 -3
  18. data/lib/identity_cache/cache_key_loader.rb +1 -1
  19. data/lib/identity_cache/cached/attribute.rb +66 -3
  20. data/lib/identity_cache/cached/attribute_by_multi.rb +94 -9
  21. data/lib/identity_cache/cached/attribute_by_one.rb +4 -40
  22. data/lib/identity_cache/cached/belongs_to.rb +14 -10
  23. data/lib/identity_cache/cached/primary_index.rb +10 -2
  24. data/lib/identity_cache/cached/recursive/association.rb +1 -1
  25. data/lib/identity_cache/cached/reference/has_many.rb +1 -1
  26. data/lib/identity_cache/configuration_dsl.rb +1 -1
  27. data/lib/identity_cache/encoder.rb +1 -0
  28. data/lib/identity_cache/fallback_fetcher.rb +5 -1
  29. data/lib/identity_cache/memoized_cache_proxy.rb +10 -0
  30. data/lib/identity_cache/parent_model_expiration.rb +8 -3
  31. data/lib/identity_cache/query_api.rb +12 -6
  32. data/lib/identity_cache/should_use_cache.rb +10 -0
  33. data/lib/identity_cache/version.rb +1 -1
  34. data/lib/identity_cache/with_primary_index.rb +27 -14
  35. data/lib/identity_cache.rb +131 -21
  36. data/shipit.rubygems.yml +4 -1
  37. metadata +13 -12
  38. data/isogun.yml +0 -11
@@ -60,14 +60,23 @@ module IdentityCache
60
60
  @cache_backend.write(key, IdentityCache::DELETED, expires_in: IdentityCache::DELETED_TTL.seconds)
61
61
  end
62
62
 
63
+ def delete_multi(keys)
64
+ key_values = keys.map { |key| [key, IdentityCache::DELETED] }.to_h
65
+ @cache_backend.write_multi(key_values, expires_in: IdentityCache::DELETED_TTL.seconds)
66
+ end
67
+
63
68
  def clear
64
69
  @cache_backend.clear
65
70
  end
66
71
 
67
72
  def fetch_multi(keys, &block)
68
- results = cas_multi(keys, &block)
69
- results = add_multi(keys, &block) if results.nil?
70
- results
73
+ if IdentityCache.should_use_cache?
74
+ results = cas_multi(keys, &block)
75
+ results = add_multi(keys, &block) if results.nil?
76
+ results
77
+ else
78
+ {}
79
+ end
71
80
  end
72
81
 
73
82
  def fetch(key, fill_lock_duration: nil, lock_wait_tries: 2, &block)
@@ -44,7 +44,7 @@ module IdentityCache
44
44
  # @param db_key [Array] Reference to what to load from the database.
45
45
  # @return [Hash] A hash mapping each database key to its corresponding value
46
46
  def load_multi(cache_fetcher, db_keys)
47
- load_batch(cache_fetcher => db_keys).fetch(cache_fetcher)
47
+ load_batch({ cache_fetcher => db_keys }).fetch(cache_fetcher)
48
48
  end
49
49
 
50
50
  # Load multiple keys for multiple cache fetchers
@@ -35,16 +35,32 @@ module IdentityCache
35
35
  end
36
36
 
37
37
  def expire(record)
38
+ all_deleted = true
39
+
38
40
  unless record.send(:was_new_record?)
39
41
  old_key = old_cache_key(record)
40
- IdentityCache.cache.delete(old_key)
42
+
43
+ if Thread.current[:idc_deferred_expiration]
44
+ Thread.current[:idc_attributes_to_expire] << old_key
45
+ # defer the deletion, and don't block the following deletion
46
+ all_deleted = true
47
+ else
48
+ all_deleted = IdentityCache.cache.delete(old_key)
49
+ end
41
50
  end
42
51
  unless record.destroyed?
43
52
  new_key = new_cache_key(record)
44
53
  if new_key != old_key
45
- IdentityCache.cache.delete(new_key)
54
+ if Thread.current[:idc_deferred_expiration]
55
+ Thread.current[:idc_attributes_to_expire] << new_key
56
+ all_deleted = true
57
+ else
58
+ all_deleted = IdentityCache.cache.delete(new_key) && all_deleted
59
+ end
46
60
  end
47
61
  end
62
+
63
+ all_deleted
48
64
  end
49
65
 
50
66
  def cache_key(index_key)
@@ -59,6 +75,48 @@ module IdentityCache
59
75
  unique ? results.first : results
60
76
  end
61
77
 
78
+ def fetch_multi(keys)
79
+ keys = keys.map { |key| cast_db_key(key) }
80
+
81
+ unless model.should_use_cache?
82
+ return load_multi_from_db(keys)
83
+ end
84
+
85
+ unordered_hash = CacheKeyLoader.load_multi(self, keys)
86
+
87
+ # Calling `values` on the result is expected to return the values in the same order as their
88
+ # corresponding keys. The fetch_multi_by_#{field_list} generated methods depend on this.
89
+ keys.each_with_object({}) do |key, ordered_hash|
90
+ ordered_hash[key] = unordered_hash.fetch(key)
91
+ end
92
+ end
93
+
94
+ def load_multi_from_db(keys)
95
+ result = {}
96
+ return result if keys.empty?
97
+
98
+ rows = load_multi_rows(keys)
99
+ default = unique ? nil : []
100
+ keys.each do |index_value|
101
+ result[index_value] = default.try!(:dup)
102
+ end
103
+ if unique
104
+ rows.each do |index_value, attribute_value|
105
+ result[index_value] = attribute_value
106
+ end
107
+ else
108
+ rows.each do |index_value, attribute_value|
109
+ result[index_value] << attribute_value
110
+ end
111
+ end
112
+ result
113
+ end
114
+
115
+ def cache_encode(db_value)
116
+ db_value
117
+ end
118
+ alias_method :cache_decode, :cache_encode
119
+
62
120
  private
63
121
 
64
122
  # @abstract
@@ -76,6 +134,11 @@ module IdentityCache
76
134
  raise NotImplementedError
77
135
  end
78
136
 
137
+ # @abstract
138
+ def load_multi_rows(_index_keys)
139
+ raise NotImplementedError
140
+ end
141
+
79
142
  # @abstract
80
143
  def cache_key_from_key_values(_key_values)
81
144
  raise NotImplementedError
@@ -101,9 +164,9 @@ module IdentityCache
101
164
  end
102
165
 
103
166
  def old_cache_key(record)
167
+ changes = record.transaction_changed_attributes
104
168
  old_key_values = key_fields.map do |field|
105
169
  field_string = field.to_s
106
- changes = record.transaction_changed_attributes
107
170
  if record.destroyed? && changes.key?(field_string)
108
171
  changes[field_string]
109
172
  elsif record.persisted? && changes.key?(field_string)
@@ -6,9 +6,14 @@ module IdentityCache
6
6
  def build
7
7
  cached_attribute = self
8
8
 
9
- model.define_singleton_method(:"fetch_#{fetch_method_suffix}") do |*key_values|
9
+ model.define_singleton_method(:"fetch_#{fetch_method_suffix}") do |*keys|
10
10
  raise_if_scoped
11
- cached_attribute.fetch(key_values)
11
+ cached_attribute.fetch(keys)
12
+ end
13
+
14
+ model.define_singleton_method(:"fetch_multi_#{fetch_method_suffix}") do |keys|
15
+ raise_if_scoped
16
+ cached_attribute.fetch_multi(keys)
12
17
  end
13
18
  end
14
19
 
@@ -16,22 +21,102 @@ module IdentityCache
16
21
 
17
22
  # Attribute method overrides
18
23
 
19
- def cast_db_key(key_values)
24
+ def cast_db_key(keys)
20
25
  field_types.each_with_index do |type, i|
21
- key_values[i] = type.cast(key_values[i])
26
+ keys[i] = type.cast(keys[i])
22
27
  end
23
- key_values
28
+ keys
24
29
  end
25
30
 
26
- def unhashed_values_cache_key_string(key_values)
27
- key_values.map { |v| v.try!(:to_s).inspect }.join("/")
31
+ def unhashed_values_cache_key_string(keys)
32
+ keys.map { |v| v.try!(:to_s).inspect }.join("/")
28
33
  end
29
34
 
30
- def load_from_db_where_conditions(key_values)
31
- Hash[key_fields.zip(key_values)]
35
+ def load_from_db_where_conditions(keys)
36
+ Hash[key_fields.zip(keys)]
37
+ end
38
+
39
+ def load_multi_rows(keys)
40
+ query = load_multi_rows_query(keys)
41
+ fields = key_fields
42
+ if (attribute_index = key_fields.index(attribute))
43
+ fields = fields.dup
44
+ fields.delete(attribute)
45
+ end
46
+
47
+ query.pluck(attribute, *fields).map do |attribute, *key_values|
48
+ key_values.insert(attribute_index, attribute) if attribute_index
49
+ [key_values, attribute]
50
+ end
32
51
  end
33
52
 
34
53
  alias_method :cache_key_from_key_values, :cache_key
54
+
55
+ # Helper methods
56
+
57
+ def load_multi_rows_query(keys)
58
+ # Find fields with a common value for the below common_query optimization
59
+ common_conditions = {}
60
+ other_field_indexes = []
61
+ key_fields.each_with_index do |field, i|
62
+ first_value = keys.first[i]
63
+ is_unique = keys.all? { |key_values| first_value == key_values[i] }
64
+
65
+ if is_unique
66
+ common_conditions[field] = first_value
67
+ else
68
+ other_field_indexes << i
69
+ end
70
+ end
71
+
72
+ common_query = if common_conditions.any?
73
+ # Optimization for the case of fields in which the key being searched
74
+ # for is always the same. This results in simple equality conditions
75
+ # being produced for these fields (e.g. "WHERE field = value").
76
+ unsorted_model.where(common_conditions)
77
+ end
78
+
79
+ case other_field_indexes.size
80
+ when 0
81
+ common_query
82
+ when 1
83
+ # Micro-optimization for the case of a single unique field.
84
+ # This results in a single "WHERE field IN (values)" statement being
85
+ # produced from a single query.
86
+ field_idx = other_field_indexes.first
87
+ field_name = key_fields[field_idx]
88
+ field_values = keys.map { |key| key[field_idx] }
89
+ (common_query || unsorted_model).where(field_name => field_values)
90
+ else
91
+ # More than one unique field, so we need to generate a query for each
92
+ # set of values for each unique field.
93
+ #
94
+ # This results in multiple
95
+ # "WHERE field = value AND field_2 = value_2 OR ..."
96
+ # statements being produced from an object like
97
+ # [{ field: value, field_2: value_2 }, ...]
98
+ query = keys.reduce(nil) do |query, key|
99
+ condition = {}
100
+ other_field_indexes.each do |field_idx|
101
+ field = key_fields[field_idx]
102
+ condition[field] = key[field_idx]
103
+ end
104
+ subquery = unsorted_model.where(condition)
105
+
106
+ query ? query.or(subquery) : subquery
107
+ end
108
+
109
+ if common_query
110
+ common_query.merge(query)
111
+ else
112
+ query
113
+ end
114
+ end
115
+ end
116
+
117
+ def unsorted_model
118
+ model.reorder(nil)
119
+ end
35
120
  end
36
121
  end
37
122
  end
@@ -24,46 +24,6 @@ module IdentityCache
24
24
  end
25
25
  end
26
26
 
27
- def fetch_multi(keys)
28
- keys = keys.map { |key| cast_db_key(key) }
29
-
30
- unless model.should_use_cache?
31
- return load_multi_from_db(keys)
32
- end
33
-
34
- unordered_hash = CacheKeyLoader.load_multi(self, keys)
35
-
36
- # Calling `values` on the result is expected to return the values in the same order as their
37
- # corresponding keys. The fetch_multi_by_#{field_list} generated methods depend on this.
38
- ordered_hash = {}
39
- keys.each { |key| ordered_hash[key] = unordered_hash.fetch(key) }
40
- ordered_hash
41
- end
42
-
43
- def load_multi_from_db(keys)
44
- rows = model.reorder(nil).where(load_from_db_where_conditions(keys)).pluck(key_field, attribute)
45
- result = {}
46
- default = unique ? nil : []
47
- keys.each do |index_value|
48
- result[index_value] = default.try!(:dup)
49
- end
50
- if unique
51
- rows.each do |index_value, attribute_value|
52
- result[index_value] = attribute_value
53
- end
54
- else
55
- rows.each do |index_value, attribute_value|
56
- result[index_value] << attribute_value
57
- end
58
- end
59
- result
60
- end
61
-
62
- def cache_encode(db_value)
63
- db_value
64
- end
65
- alias_method :cache_decode, :cache_encode
66
-
67
27
  private
68
28
 
69
29
  # Attribute method overrides
@@ -80,6 +40,10 @@ module IdentityCache
80
40
  { key_field => key_values }
81
41
  end
82
42
 
43
+ def load_multi_rows(keys)
44
+ model.reorder(nil).where(load_from_db_where_conditions(keys)).pluck(key_field, attribute)
45
+ end
46
+
83
47
  def cache_key_from_key_values(key_values)
84
48
  cache_key(key_values.first)
85
49
  end
@@ -9,7 +9,7 @@ module IdentityCache
9
9
  reflection.active_record.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
10
10
  def #{cached_accessor_name}
11
11
  association_klass = association(:#{name}).klass
12
- if association_klass.should_use_cache? && #{reflection.foreign_key}.present? && !association(:#{name}).loaded?
12
+ if #{reflection.foreign_key}.present? && !association(:#{name}).loaded? && (loaded_by_idc? || association_klass.should_use_cache?)
13
13
  if defined?(#{records_variable_name})
14
14
  #{records_variable_name}
15
15
  else
@@ -77,16 +77,20 @@ module IdentityCache
77
77
  end
78
78
  end
79
79
 
80
- load_strategy.load_multi(
81
- reflection.klass.cached_primary_index,
82
- ids_to_owner_record.keys
83
- ) do |associated_records_by_id|
84
- associated_records_by_id.each do |id, associated_record|
85
- owner_record = ids_to_owner_record.fetch(id)
86
- write(owner_record, associated_record)
87
- end
80
+ if ids_to_owner_record.any?
81
+ load_strategy.load_multi(
82
+ reflection.klass.cached_primary_index,
83
+ ids_to_owner_record.keys
84
+ ) do |associated_records_by_id|
85
+ associated_records_by_id.each do |id, associated_record|
86
+ owner_record = ids_to_owner_record.fetch(id)
87
+ write(owner_record, associated_record)
88
+ end
88
89
 
89
- yield associated_records_by_id.values.compact
90
+ yield associated_records_by_id.values.compact
91
+ end
92
+ else
93
+ yield records.filter_map { |record| record.instance_variable_get(records_variable_name) }
90
94
  end
91
95
  end
92
96
  end
@@ -45,7 +45,11 @@ module IdentityCache
45
45
 
46
46
  def expire(id)
47
47
  id = cast_id(id)
48
- IdentityCache.cache.delete(cache_key(id))
48
+ if Thread.current[:idc_deferred_expiration]
49
+ Thread.current[:idc_records_to_expire] << cache_key(id)
50
+ else
51
+ IdentityCache.cache.delete(cache_key(id))
52
+ end
49
53
  end
50
54
 
51
55
  def cache_key(id)
@@ -54,7 +58,10 @@ module IdentityCache
54
58
 
55
59
  def load_one_from_db(id)
56
60
  record = build_query(id).take
57
- model.send(:setup_embedded_associations_on_miss, [record]) if record
61
+ if record
62
+ model.send(:setup_embedded_associations_on_miss, [record])
63
+ record.send(:mark_as_loaded_by_idc)
64
+ end
58
65
  record
59
66
  end
60
67
 
@@ -63,6 +70,7 @@ module IdentityCache
63
70
 
64
71
  records = build_query(ids).to_a
65
72
  model.send(:setup_embedded_associations_on_miss, records)
73
+ records.each { |record| record.send(:mark_as_loaded_by_idc) }
66
74
  records.index_by(&:id)
67
75
  end
68
76
 
@@ -25,7 +25,7 @@ module IdentityCache
25
25
  def read(record)
26
26
  assoc = record.association(name)
27
27
 
28
- if assoc.klass.should_use_cache? && !assoc.loaded? && assoc.target.blank?
28
+ if !assoc.loaded? && assoc.target.blank? && (record.send(:loaded_by_idc?) || assoc.klass.should_use_cache?)
29
29
  if record.instance_variable_defined?(records_variable_name)
30
30
  record.instance_variable_get(records_variable_name)
31
31
  elsif record.instance_variable_defined?(dehydrated_variable_name)
@@ -22,7 +22,7 @@ module IdentityCache
22
22
 
23
23
  def #{cached_accessor_name}
24
24
  assoc = association(:#{name})
25
- if assoc.klass.should_use_cache? && !assoc.loaded? && assoc.target.blank?
25
+ if (loaded_by_idc? || assoc.klass.should_use_cache?) && !assoc.loaded? && assoc.target.blank?
26
26
  #{records_variable_name} ||= #{reflection.class_name}.fetch_multi(#{cached_ids_name})
27
27
  else
28
28
  #{name}.to_a
@@ -143,7 +143,7 @@ module IdentityCache
143
143
 
144
144
  def check_association_for_caching(association)
145
145
  unless (association_reflection = reflect_on_association(association))
146
- raise AssociationError, "Association named '#{association}' was not found on #{self.class}"
146
+ raise AssociationError, "Association named '#{association}' was not found on #{self}"
147
147
  end
148
148
  if association_reflection.options[:through]
149
149
  raise UnsupportedAssociationError, "caching through associations isn't supported"
@@ -69,6 +69,7 @@ module IdentityCache
69
69
 
70
70
  def record_from_coder(coder, klass) # :nodoc:
71
71
  record = klass.instantiate(coder[:attributes].dup)
72
+ record.send(:mark_as_loaded_by_idc)
72
73
 
73
74
  if coder.key?(:associations)
74
75
  coder[:associations].each do |name, value|
@@ -21,7 +21,11 @@ module IdentityCache
21
21
  end
22
22
 
23
23
  def fetch_multi(keys)
24
- results = @cache_backend.read_multi(*keys)
24
+ results = if IdentityCache.should_use_cache?
25
+ @cache_backend.read_multi(*keys)
26
+ else
27
+ {}
28
+ end
25
29
  missed_keys = keys - results.keys
26
30
  unless missed_keys.empty?
27
31
  replacement_results = yield missed_keys
@@ -70,6 +70,16 @@ module IdentityCache
70
70
  end
71
71
  end
72
72
 
73
+ def delete_multi(keys)
74
+ memoizing = memoizing?
75
+ ActiveSupport::Notifications.instrument("cache_delete_multi.identity_cache", memoizing: memoizing) do
76
+ if memoizing
77
+ keys.each { |key| memoized_key_values.delete(key) }
78
+ end
79
+ @cache_fetcher.delete_multi(keys)
80
+ end
81
+ end
82
+
73
83
  def fetch(key, cache_fetcher_options = {}, &block)
74
84
  memo_misses = 0
75
85
  cache_misses = 0
@@ -45,8 +45,13 @@ module IdentityCache
45
45
  def expire_parent_caches
46
46
  parents_to_expire = Set.new
47
47
  add_parents_to_cache_expiry_set(parents_to_expire)
48
- parents_to_expire.each do |parent|
49
- parent.expire_primary_index if parent.class.primary_cache_index_enabled
48
+ parents_to_expire.select! { |parent| parent.class.primary_cache_index_enabled }
49
+ parents_to_expire.reduce(true) do |all_expired, parent|
50
+ if Thread.current[:idc_deferred_parent_expiration]
51
+ Thread.current[:idc_parent_records_for_cache_expiry] << parent
52
+ next parent
53
+ end
54
+ parent.expire_primary_index && all_expired
50
55
  end
51
56
  end
52
57
 
@@ -65,7 +70,7 @@ module IdentityCache
65
70
 
66
71
  def parents_to_expire_on_changes(parents_to_expire, association_name, cached_associations)
67
72
  parent_association = self.class.reflect_on_association(association_name)
68
- foreign_key = parent_association.association_foreign_key
73
+ foreign_key = parent_association.foreign_key
69
74
 
70
75
  new_parent = send(association_name)
71
76
 
@@ -70,6 +70,8 @@ module IdentityCache
70
70
  readonly: IdentityCache.fetch_read_only_records && should_use_cache?)
71
71
  return if records.empty?
72
72
 
73
+ return unless should_use_cache?
74
+
73
75
  records.each(&:readonly!) if readonly
74
76
  each_id_embedded_association do |cached_association|
75
77
  preload_id_embedded_association(records, cached_association)
@@ -166,15 +168,17 @@ module IdentityCache
166
168
  def _run_commit_callbacks
167
169
  if destroyed? || transaction_changed_attributes.present?
168
170
  expire_cache
169
- expire_parent_caches
170
171
  end
171
172
  super
172
173
  end
173
174
 
174
- # Invalidate the cache data associated with the record.
175
+ # Invalidate the cache data associated with the record. Returns `true` on success,
176
+ # `false` otherwise.
175
177
  def expire_cache
176
- expire_attribute_indexes
177
- true
178
+ expired_parent_caches = expire_parent_caches
179
+ expired_attribute_indexes = expire_attribute_indexes
180
+
181
+ expired_parent_caches && expired_attribute_indexes
178
182
  end
179
183
 
180
184
  # @api private
@@ -185,9 +189,11 @@ module IdentityCache
185
189
 
186
190
  private
187
191
 
192
+ # Even if we have problems with some attributes, carry over the results and expire
193
+ # all possible attributes without array allocation.
188
194
  def expire_attribute_indexes # :nodoc:
189
- cache_indexes.each do |cached_attribute|
190
- cached_attribute.expire(self)
195
+ cache_indexes.reduce(true) do |all_expired, cached_attribute|
196
+ cached_attribute.expire(self) && all_expired
191
197
  end
192
198
  end
193
199
  end
@@ -9,5 +9,15 @@ module IdentityCache
9
9
  IdentityCache.should_use_cache?
10
10
  end
11
11
  end
12
+
13
+ private
14
+
15
+ def mark_as_loaded_by_idc
16
+ @loaded_by_idc = true
17
+ end
18
+
19
+ def loaded_by_idc?
20
+ defined?(@loaded_by_idc)
21
+ end
12
22
  end
13
23
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IdentityCache
4
- VERSION = "1.2.0"
4
+ VERSION = "1.6.3"
5
5
  CACHE_VERSION = 8
6
6
  end
@@ -7,8 +7,9 @@ module IdentityCache
7
7
  include WithoutPrimaryIndex
8
8
 
9
9
  def expire_cache
10
- expire_primary_index
11
- super
10
+ expired_primary_index = expire_primary_index
11
+
12
+ super && expired_primary_index
12
13
  end
13
14
 
14
15
  # @api private
@@ -34,8 +35,8 @@ module IdentityCache
34
35
  # Declares a new index in the cache for the class where IdentityCache was
35
36
  # included.
36
37
  #
37
- # IdentityCache will add a fetch_by_field1_and_field2_and_...field for every
38
- # index.
38
+ # IdentityCache will add a fetch_by_field1_and_field2_and_...field and
39
+ # fetch_multi_by_field1_and_field2_and_...field for every index.
39
40
  #
40
41
  # == Example:
41
42
  #
@@ -44,7 +45,10 @@ module IdentityCache
44
45
  # cache_index :name, :vendor
45
46
  # end
46
47
  #
47
- # Will add Product.fetch_by_name_and_vendor
48
+ # Will add:
49
+ #
50
+ # Product.fetch_by_name_and_vendor
51
+ # Product.fetch_multi_by_name_and_vendor
48
52
  #
49
53
  # == Parameters
50
54
  #
@@ -81,15 +85,13 @@ module IdentityCache
81
85
  CODE
82
86
  end
83
87
 
84
- if fields.length == 1
85
- instance_eval(<<-CODE, __FILE__, __LINE__ + 1)
86
- def fetch_multi_by_#{field_list}(index_values, includes: nil)
87
- ids = fetch_multi_id_by_#{field_list}(index_values).values.flatten(1)
88
- return ids if ids.empty?
89
- fetch_multi(ids, includes: includes)
90
- end
91
- CODE
92
- end
88
+ instance_eval(<<-CODE, __FILE__, __LINE__ + 1)
89
+ def fetch_multi_by_#{field_list}(index_values, includes: nil)
90
+ ids = fetch_multi_id_by_#{field_list}(index_values).values.flatten(1)
91
+ return ids if ids.empty?
92
+ fetch_multi(ids, includes: includes)
93
+ end
94
+ CODE
93
95
  end
94
96
 
95
97
  # Similar to ActiveRecord::Base#exists? will return true if the id can be
@@ -149,6 +151,8 @@ module IdentityCache
149
151
  ensure_base_model
150
152
  raise_if_scoped
151
153
  ids.flatten!(1)
154
+ return [] if ids.none?
155
+
152
156
  records = cached_primary_index.fetch_multi(ids)
153
157
  prefetch_associations(includes, records) if includes
154
158
  records
@@ -158,6 +162,15 @@ module IdentityCache
158
162
  def expire_primary_key_cache_index(id)
159
163
  cached_primary_index.expire(id)
160
164
  end
165
+
166
+ private
167
+
168
+ def inherited(subclass)
169
+ super
170
+ subclass.class_eval do
171
+ @cached_primary_index = nil
172
+ end
173
+ end
161
174
  end
162
175
  end
163
176
  end