identity_cache 1.3.1 → 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3abed29487ae94532060d1a35e2bc86f8b4187cbc9a3074c8c304ab4f8426b4b
4
- data.tar.gz: ddcaf494c9bc48055ee4e1ddc83a5caf61e047e4d053eb2b4e87a086a73f97ae
3
+ metadata.gz: acfed214f7a98554a2a32f066e1de02da09c69f2ccd46cd5cfbc2b343ecebe30
4
+ data.tar.gz: 428ce88a990db844ea32dab391b85cf65046af1a5f4462b2596752637ab94ee7
5
5
  SHA512:
6
- metadata.gz: e0c8e9d0996d6f7f3f3e760fbf1ec2f7b8fa819bbeeaa3dc8b1286125265da45c6b174a77d3b13f763e6db104d613a55562f0a0ff13eb6d0c800e455ae30020c
7
- data.tar.gz: 03a5cffffa5a0971f0d12a4d449c518a471d579410c2f7b8dfe32ad82234fb08115fa44cc99e726ed28352ead0287c5e966f47fb0b607758ddb29b1baa4667a4
6
+ metadata.gz: febcac0077ff5e20d9e6c42dd472c147dca5baa5e37a86db7f3c2839ac61020cb770bb781ecef98d9d8c7cf47b347b3f7810210c5f1e620fc4f052ba5cf6496f
7
+ data.tar.gz: 4f630185bd4c0b5cf372de27aed7e40d484c94b35ae6f271d89d45e02adf7fa9648ad5151714e9646d01f559f05f2cdd989af6edb6536633f72640133f6b4de9
data/CHANGELOG.md CHANGED
@@ -2,33 +2,51 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.4.1
6
+
7
+ ### Fixes
8
+
9
+ - Fix `fetch_multi_by` bug for queries having a single field with distinct values. (#536)
10
+
11
+ ## 1.4.0
12
+
13
+ ### Features
14
+
15
+ - Add `fetch_multi_by` support for composite-key indexes. (#534)
16
+
5
17
  ## 1.3.1
6
18
 
7
19
  ### Fixes
20
+
8
21
  - Remove N+1 queries from embedded associations when using `fetch` while `should_use_cache` is false. (#531)
9
22
 
10
23
  ## 1.3.0
11
24
 
12
25
  ### Features
26
+
13
27
  - Return meaningful value from `expire_cache` indicating whenever it succeeded or failed in the process. (#523)
14
28
 
15
29
  ### Fixes
30
+
16
31
  - Expire parents cache when when calling `expire_cache`. (#523)
17
32
  - Avoid creating too many shapes on Ruby 3.2+. (#526)
18
33
 
19
34
  ## 1.2.0
20
35
 
21
36
  ### Fixes
37
+
22
38
  - Fix mem_cache_store adapter with pool_size (#489)
23
39
  - Fix dalli deprecation warning about requiring 'dalli/cas/client' (#511)
24
40
  - Make transitionary method IdentityCache.with_fetch_read_only_records thread-safe (#503)
25
41
 
26
42
  ### Features
43
+
27
44
  - Add support for fill lock with lock wait to avoid thundering herd problem (#373)
28
45
 
29
46
  ## 1.1.0
30
47
 
31
48
  ### Fixes
49
+
32
50
  - Fix double debug logging of cache hits and misses (#474)
33
51
  - Fix a Rails 6.1 deprecation warning for Rails 7.0 compatibility (#482)
34
52
  - Recursively install parent expiry hooks when expiring parent caches (#476)
@@ -40,10 +58,12 @@
40
58
  - Fix fetch `has_many` embedded association on record after adding to it (#449)
41
59
 
42
60
  ### Features
61
+
43
62
  - Support multiple databases and transactional tests in `IdentityCache.should_use_cache?` (#293)
44
63
  - Add support for the default `MemCacheStore` from `ActiveSupport` (#465)
45
64
 
46
65
  ### Breaking Changes
66
+
47
67
  - Drop ruby 2.4 support, since it is no longer supported upstream (#468)
48
68
 
49
69
  ## 1.0.1
@@ -115,7 +135,7 @@
115
135
  - Remove support for 3.2
116
136
  - Fix N+1 from fetching embedded ids on a cache miss
117
137
  - Raise when trying to cache a through association. Previously it wouldn't be invalidated properly.
118
- - Raise if a class method is called on a scope. Previously the scope was ignored.
138
+ - Raise if a class method is called on a scope. Previously the scope was ignored.
119
139
  - Raise if a class method is called on a subclass of one that included IdentityCache. This never worked properly.
120
140
  - Fix cache_belongs_to on polymorphic assocations.
121
141
  - Fetching a cache_belongs_to association no longer loads the belongs_to association
@@ -176,7 +196,6 @@
176
196
 
177
197
  ## 0.0.5
178
198
 
179
-
180
199
  ## 0.0.4
181
200
 
182
201
  - Fix: only marshal attributes, embedded associations and normalized association IDs
data/README.md CHANGED
@@ -101,7 +101,15 @@ product = Product.fetch_by_handle(handle)
101
101
  # Fetch multiple products by providing an array of index values.
102
102
  products = Product.fetch_multi_by_handle(handles)
103
103
 
104
+ # Fetch a single product by providing composite attributes.
104
105
  products = Product.fetch_by_vendor_and_product_type(vendor, product_type)
106
+
107
+ # Fetch multiple products by providing an array of composite attributes.
108
+ products = Product.fetch_multi_by_vendor_and_product_type([
109
+ [vendor_1, product_type_1],
110
+ [vendor_2, product_type_2],
111
+ # ...
112
+ ])
105
113
  ```
106
114
 
107
115
  This gives you a lot of freedom to use your objects the way you want to, and doesn't get in your way. This does keep an independent cache copy in Memcached so you might want to watch the number of different caches that are being added.
@@ -63,6 +63,48 @@ module IdentityCache
63
63
  unique ? results.first : results
64
64
  end
65
65
 
66
+ def fetch_multi(keys)
67
+ keys = keys.map { |key| cast_db_key(key) }
68
+
69
+ unless model.should_use_cache?
70
+ return load_multi_from_db(keys)
71
+ end
72
+
73
+ unordered_hash = CacheKeyLoader.load_multi(self, keys)
74
+
75
+ # Calling `values` on the result is expected to return the values in the same order as their
76
+ # corresponding keys. The fetch_multi_by_#{field_list} generated methods depend on this.
77
+ keys.each_with_object({}) do |key, ordered_hash|
78
+ ordered_hash[key] = unordered_hash.fetch(key)
79
+ end
80
+ end
81
+
82
+ def load_multi_from_db(keys)
83
+ result = {}
84
+ return result if keys.empty?
85
+
86
+ rows = load_multi_rows(keys)
87
+ default = unique ? nil : []
88
+ keys.each do |index_value|
89
+ result[index_value] = default.try!(:dup)
90
+ end
91
+ if unique
92
+ rows.each do |index_value, attribute_value|
93
+ result[index_value] = attribute_value
94
+ end
95
+ else
96
+ rows.each do |index_value, attribute_value|
97
+ result[index_value] << attribute_value
98
+ end
99
+ end
100
+ result
101
+ end
102
+
103
+ def cache_encode(db_value)
104
+ db_value
105
+ end
106
+ alias_method :cache_decode, :cache_encode
107
+
66
108
  private
67
109
 
68
110
  # @abstract
@@ -80,6 +122,11 @@ module IdentityCache
80
122
  raise NotImplementedError
81
123
  end
82
124
 
125
+ # @abstract
126
+ def load_multi_rows(_index_keys)
127
+ raise NotImplementedError
128
+ end
129
+
83
130
  # @abstract
84
131
  def cache_key_from_key_values(_key_values)
85
132
  raise NotImplementedError
@@ -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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IdentityCache
4
- VERSION = "1.3.1"
4
+ VERSION = "1.4.1"
5
5
  CACHE_VERSION = 8
6
6
  end
@@ -35,8 +35,8 @@ module IdentityCache
35
35
  # Declares a new index in the cache for the class where IdentityCache was
36
36
  # included.
37
37
  #
38
- # IdentityCache will add a fetch_by_field1_and_field2_and_...field for every
39
- # 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.
40
40
  #
41
41
  # == Example:
42
42
  #
@@ -45,7 +45,10 @@ module IdentityCache
45
45
  # cache_index :name, :vendor
46
46
  # end
47
47
  #
48
- # 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
49
52
  #
50
53
  # == Parameters
51
54
  #
@@ -82,15 +85,13 @@ module IdentityCache
82
85
  CODE
83
86
  end
84
87
 
85
- if fields.length == 1
86
- instance_eval(<<-CODE, __FILE__, __LINE__ + 1)
87
- def fetch_multi_by_#{field_list}(index_values, includes: nil)
88
- ids = fetch_multi_id_by_#{field_list}(index_values).values.flatten(1)
89
- return ids if ids.empty?
90
- fetch_multi(ids, includes: includes)
91
- end
92
- CODE
93
- 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
94
95
  end
95
96
 
96
97
  # Similar to ActiveRecord::Base#exists? will return true if the id can be
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: identity_cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Camilo Lopez
@@ -14,7 +14,7 @@ authors:
14
14
  autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
- date: 2023-03-23 00:00:00.000000000 Z
17
+ date: 2023-04-12 00:00:00.000000000 Z
18
18
  dependencies:
19
19
  - !ruby/object:Gem::Dependency
20
20
  name: activerecord
@@ -190,7 +190,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
190
190
  - !ruby/object:Gem::Version
191
191
  version: '0'
192
192
  requirements: []
193
- rubygems_version: 3.4.9
193
+ rubygems_version: 3.4.10
194
194
  signing_key:
195
195
  specification_version: 4
196
196
  summary: IdentityCache lets you specify how you want to cache your model objects,