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 +4 -4
- data/CHANGELOG.md +21 -2
- data/README.md +8 -0
- data/lib/identity_cache/cached/attribute.rb +47 -0
- data/lib/identity_cache/cached/attribute_by_multi.rb +94 -9
- data/lib/identity_cache/cached/attribute_by_one.rb +4 -40
- data/lib/identity_cache/version.rb +1 -1
- data/lib/identity_cache/with_primary_index.rb +13 -12
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: acfed214f7a98554a2a32f066e1de02da09c69f2ccd46cd5cfbc2b343ecebe30
|
4
|
+
data.tar.gz: 428ce88a990db844ea32dab391b85cf65046af1a5f4462b2596752637ab94ee7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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 |*
|
9
|
+
model.define_singleton_method(:"fetch_#{fetch_method_suffix}") do |*keys|
|
10
10
|
raise_if_scoped
|
11
|
-
cached_attribute.fetch(
|
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(
|
24
|
+
def cast_db_key(keys)
|
20
25
|
field_types.each_with_index do |type, i|
|
21
|
-
|
26
|
+
keys[i] = type.cast(keys[i])
|
22
27
|
end
|
23
|
-
|
28
|
+
keys
|
24
29
|
end
|
25
30
|
|
26
|
-
def unhashed_values_cache_key_string(
|
27
|
-
|
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(
|
31
|
-
Hash[key_fields.zip(
|
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
|
@@ -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
|
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
|
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
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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.
|
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-
|
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.
|
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,
|