redis-objects-preloadable 0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +128 -0
- data/lib/redis/objects/preloadable/model_extension.rb +38 -0
- data/lib/redis/objects/preloadable/preload_context.rb +104 -0
- data/lib/redis/objects/preloadable/relation_extension.rb +33 -0
- data/lib/redis/objects/preloadable/type_patches/counter.rb +29 -0
- data/lib/redis/objects/preloadable/type_patches/hash_key.rb +43 -0
- data/lib/redis/objects/preloadable/type_patches/list.rb +49 -0
- data/lib/redis/objects/preloadable/type_patches/set.rb +43 -0
- data/lib/redis/objects/preloadable/type_patches/sorted_set.rb +50 -0
- data/lib/redis/objects/preloadable/type_patches/value.rb +29 -0
- data/lib/redis/objects/preloadable/version.rb +9 -0
- data/lib/redis/objects/preloadable.rb +45 -0
- metadata +89 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2e46629bdca1958195bdb6eebd11446ae6842a574b8789855fdede06cac509fe
|
|
4
|
+
data.tar.gz: 5730090a0ffd1a8697c061d6a714d6f48a75cc140b809d2377364925d2ace50b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 79589dae4c2e987e95d3d9195f00752da234a5f5b0ec37bd0351c9f5d20be83136836784e2627fdde55fead3e30b384a5e0ff8705a6ccc63128c75f4f5f8a820
|
|
7
|
+
data.tar.gz: 6601dcdf77506ebdc93e574ce9e9b3e469c9538a8376d70d134fa75b7b8fe968a97fd38158026ec66c172344b1f0f00ec03b1dbcd9db09595aaebe01d13e7a29
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kyohah
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# redis-objects-preloadable
|
|
2
|
+
|
|
3
|
+
[English](README.md) | [日本語](docs/ja/index.md) | [中文](docs/zh/index.md) | [Français](docs/fr/index.md) | [Deutsch](docs/de/index.md)
|
|
4
|
+
|
|
5
|
+
Eliminate N+1 Redis calls for [redis-objects](https://github.com/nateware/redis-objects) in ActiveRecord models.
|
|
6
|
+
|
|
7
|
+
Provides `redis_preload` scope that batch-loads Redis::Objects attributes using `MGET` (for counter/value) and `pipelined` commands (for list/set/sorted_set/hash_key), following the same design as ActiveRecord's `preload`.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "redis-objects-preloadable"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
Include `Redis::Objects::Preloadable` in your model after `Redis::Objects`:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
class Pack < ApplicationRecord
|
|
21
|
+
include Redis::Objects
|
|
22
|
+
include Redis::Objects::Preloadable
|
|
23
|
+
|
|
24
|
+
counter :cache_total_count, expiration: 15.minutes
|
|
25
|
+
list :recent_item_ids
|
|
26
|
+
set :tag_ids
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
Chain `redis_preload` onto any ActiveRecord relation:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
records = Pack.order(:id)
|
|
36
|
+
.redis_preload(:cache_total_count, :recent_item_ids, :tag_ids)
|
|
37
|
+
.limit(100)
|
|
38
|
+
|
|
39
|
+
records.each do |pack|
|
|
40
|
+
pack.cache_total_count.value # preloaded, no Redis call
|
|
41
|
+
pack.recent_item_ids.values # preloaded
|
|
42
|
+
pack.tag_ids.members # preloaded
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Without `redis_preload`, accessing Redis attributes falls back to individual Redis calls (original behavior).
|
|
47
|
+
|
|
48
|
+
### Preloading on Association-Loaded Records
|
|
49
|
+
|
|
50
|
+
`redis_preload` works on top-level relations. For records loaded via `includes` / `preload` / `eager_load`, use `Redis::Objects::Preloadable.preload`:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
class User < ApplicationRecord
|
|
54
|
+
has_many :articles
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class Article < ApplicationRecord
|
|
58
|
+
include Redis::Objects
|
|
59
|
+
include Redis::Objects::Preloadable
|
|
60
|
+
|
|
61
|
+
counter :view_count
|
|
62
|
+
value :cached_summary
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
users = User.includes(:articles).load
|
|
66
|
+
|
|
67
|
+
# Batch-preload Redis attributes on the associated records
|
|
68
|
+
articles = users.flat_map(&:articles)
|
|
69
|
+
Redis::Objects::Preloadable.preload(articles, :view_count, :cached_summary)
|
|
70
|
+
|
|
71
|
+
users.each do |user|
|
|
72
|
+
user.articles.each do |article|
|
|
73
|
+
article.view_count.value # preloaded, no Redis call
|
|
74
|
+
article.cached_summary.value # preloaded
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`Redis::Objects::Preloadable.preload` accepts any array of records, so it works in any context — not just associations.
|
|
80
|
+
|
|
81
|
+
### Lazy Resolution
|
|
82
|
+
|
|
83
|
+
Preloading is lazy. The `redis_preload` scope attaches metadata to the relation, but no Redis calls are made until you first access a preloaded attribute. At that point, all declared attributes for all loaded records are fetched in a single batch.
|
|
84
|
+
|
|
85
|
+
## Supported Types
|
|
86
|
+
|
|
87
|
+
| redis-objects type | Redis command | Preloaded methods |
|
|
88
|
+
|--------------------|-------------------|--------------------------------------------|
|
|
89
|
+
| `counter` | MGET | `value`, `nil?` |
|
|
90
|
+
| `value` | MGET | `value`, `nil?` |
|
|
91
|
+
| `list` | LRANGE 0 -1 | `value`, `values`, `[]`, `length`, `empty?`|
|
|
92
|
+
| `set` | SMEMBERS | `members`, `include?`, `length`, `empty?` |
|
|
93
|
+
| `sorted_set` | ZRANGE WITHSCORES | `members`, `score`, `rank`, `length` |
|
|
94
|
+
| `hash_key` | HGETALL | `all`, `[]`, `keys`, `values` |
|
|
95
|
+
|
|
96
|
+
## How It Works
|
|
97
|
+
|
|
98
|
+
1. `redis_preload(:attr1, :attr2)` extends the AR relation with `RelationExtension`
|
|
99
|
+
2. When the relation is loaded, a `PreloadContext` is attached to each redis-objects instance
|
|
100
|
+
3. On first attribute access, `PreloadContext#resolve!` fires:
|
|
101
|
+
- **counter/value** types: batched via `MGET`
|
|
102
|
+
- **list/set/sorted_set/hash_key** types: batched via `pipelined`
|
|
103
|
+
4. Each redis-objects instance receives its preloaded value via `preload!`
|
|
104
|
+
5. Subsequent reads return the preloaded value without hitting Redis
|
|
105
|
+
|
|
106
|
+
The type patches are applied via `prepend` on `Redis::Counter`, `Redis::Value`, `Redis::List`, `Redis::Set`, `Redis::SortedSet`, and `Redis::HashKey`.
|
|
107
|
+
|
|
108
|
+
## Backward Compatibility: `read_redis_counter`
|
|
109
|
+
|
|
110
|
+
If your models use the `read_redis_counter` helper (from the original Concern-based approach), it continues to work. With transparent preloading, you can remove explicit `read_redis_counter` calls and access `counter.value` directly.
|
|
111
|
+
|
|
112
|
+
## Requirements
|
|
113
|
+
|
|
114
|
+
- Ruby >= 3.1
|
|
115
|
+
- ActiveRecord >= 7.0
|
|
116
|
+
- redis-objects >= 1.7
|
|
117
|
+
|
|
118
|
+
## Development
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
bin/setup # install dependencies
|
|
122
|
+
bundle exec rspec # run tests (requires local Redis)
|
|
123
|
+
bundle exec rubocop # lint
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Redis
|
|
4
|
+
module Objects
|
|
5
|
+
module Preloadable
|
|
6
|
+
module ModelExtension
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
class_methods do
|
|
10
|
+
def all
|
|
11
|
+
super.extending(Redis::Objects::Preloadable::RelationExtension)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def read_redis_counter(_name, counter)
|
|
18
|
+
if counter.instance_variable_defined?(:@preloaded_value)
|
|
19
|
+
raw = counter.instance_variable_get(:@preloaded_value)
|
|
20
|
+
return raw.to_i if raw
|
|
21
|
+
|
|
22
|
+
count = yield
|
|
23
|
+
counter.value = count
|
|
24
|
+
return count
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if counter.exists?
|
|
28
|
+
counter.value.to_i
|
|
29
|
+
else
|
|
30
|
+
count = yield
|
|
31
|
+
counter.value = count
|
|
32
|
+
count
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Redis
|
|
4
|
+
module Objects
|
|
5
|
+
module Preloadable
|
|
6
|
+
MGET_TYPES = %i[counter value].freeze
|
|
7
|
+
|
|
8
|
+
class PreloadContext
|
|
9
|
+
def initialize(records, names)
|
|
10
|
+
@records = records
|
|
11
|
+
@names = names
|
|
12
|
+
@resolved = false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def resolve!
|
|
16
|
+
return if @resolved
|
|
17
|
+
|
|
18
|
+
@resolved = true
|
|
19
|
+
return if @records.empty?
|
|
20
|
+
|
|
21
|
+
execute_preload
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def execute_preload
|
|
27
|
+
klass = @records.first.class
|
|
28
|
+
prefix = klass.redis_prefix
|
|
29
|
+
ids = @records.map(&:id)
|
|
30
|
+
redis_objects = klass.redis_objects
|
|
31
|
+
|
|
32
|
+
mget_names = @names.select { |n| MGET_TYPES.include?(redis_objects.dig(n, :type)) }
|
|
33
|
+
pipeline_names = @names - mget_names
|
|
34
|
+
|
|
35
|
+
with_redis do |redis|
|
|
36
|
+
fetch_mget(redis, prefix, ids, mget_names)
|
|
37
|
+
fetch_pipeline(redis, prefix, ids, pipeline_names, redis_objects)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def with_redis(&)
|
|
42
|
+
redis_conn = ::Redis::Objects.redis
|
|
43
|
+
if redis_conn.respond_to?(:with)
|
|
44
|
+
redis_conn.with(&)
|
|
45
|
+
else
|
|
46
|
+
yield redis_conn
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def fetch_mget(redis, prefix, ids, names)
|
|
51
|
+
return if names.empty?
|
|
52
|
+
|
|
53
|
+
n = ids.size
|
|
54
|
+
keys = names.flat_map { |name| ids.map { |id| "#{prefix}:#{id}:#{name}" } }
|
|
55
|
+
values = redis.mget(*keys)
|
|
56
|
+
|
|
57
|
+
names.each_with_index do |name, i|
|
|
58
|
+
@records.zip(values[i * n, n]).each do |record, raw_value|
|
|
59
|
+
record.public_send(name).preload!(raw_value)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def fetch_pipeline(redis, prefix, ids, names, redis_objects)
|
|
65
|
+
return if names.empty?
|
|
66
|
+
|
|
67
|
+
order, results = run_pipeline(redis, prefix, ids, names, redis_objects)
|
|
68
|
+
apply_pipeline_results(order, results)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def run_pipeline(redis, prefix, ids, names, redis_objects)
|
|
72
|
+
order = []
|
|
73
|
+
results = redis.pipelined do |pipe|
|
|
74
|
+
names.each do |name|
|
|
75
|
+
type = redis_objects.dig(name, :type)
|
|
76
|
+
ids.each do |id|
|
|
77
|
+
order << [name, id]
|
|
78
|
+
pipeline_command(pipe, "#{prefix}:#{id}:#{name}", type)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
[order, results]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def pipeline_command(pipe, key, type)
|
|
86
|
+
case type
|
|
87
|
+
when :list then pipe.lrange(key, 0, -1)
|
|
88
|
+
when :set then pipe.smembers(key)
|
|
89
|
+
when :sorted_set then pipe.zrange(key, 0, -1, with_scores: true)
|
|
90
|
+
when :dict then pipe.hgetall(key)
|
|
91
|
+
else pipe.get(key)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def apply_pipeline_results(order, results)
|
|
96
|
+
record_by_id = @records.index_by(&:id)
|
|
97
|
+
order.each_with_index do |(name, id), idx|
|
|
98
|
+
record_by_id[id]&.public_send(name)&.preload!(results[idx])
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Redis
|
|
4
|
+
module Objects
|
|
5
|
+
module Preloadable
|
|
6
|
+
module RelationExtension
|
|
7
|
+
def redis_preload(*names)
|
|
8
|
+
spawn.tap { |r| r.instance_variable_set(:@redis_preload_names, names) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def redis_preload_names
|
|
12
|
+
@redis_preload_names || []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def load
|
|
16
|
+
result = super
|
|
17
|
+
|
|
18
|
+
if !@redis_preload_context_attached && @redis_preload_names&.any? && loaded?
|
|
19
|
+
@redis_preload_context_attached = true
|
|
20
|
+
context = PreloadContext.new(records, @redis_preload_names)
|
|
21
|
+
records.each do |record|
|
|
22
|
+
@redis_preload_names.each do |name|
|
|
23
|
+
record.public_send(name).instance_variable_set(:@preload_context, context)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
result
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Redis
|
|
4
|
+
module Objects
|
|
5
|
+
module Preloadable
|
|
6
|
+
module TypePatches
|
|
7
|
+
module Counter
|
|
8
|
+
def preload!(raw_value)
|
|
9
|
+
@preloaded_value = raw_value
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def value
|
|
13
|
+
@preload_context&.resolve!
|
|
14
|
+
return @preloaded_value.to_i if defined?(@preloaded_value)
|
|
15
|
+
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def nil?
|
|
20
|
+
@preload_context&.resolve!
|
|
21
|
+
return @preloaded_value.nil? if defined?(@preloaded_value)
|
|
22
|
+
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Redis
|
|
4
|
+
module Objects
|
|
5
|
+
module Preloadable
|
|
6
|
+
module TypePatches
|
|
7
|
+
module HashKey
|
|
8
|
+
def preload!(raw_value)
|
|
9
|
+
@preloaded_value = raw_value || {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def all
|
|
13
|
+
@preload_context&.resolve!
|
|
14
|
+
return @preloaded_value if defined?(@preloaded_value)
|
|
15
|
+
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def [](key)
|
|
20
|
+
@preload_context&.resolve!
|
|
21
|
+
return @preloaded_value[key.to_s] if defined?(@preloaded_value)
|
|
22
|
+
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def keys
|
|
27
|
+
@preload_context&.resolve!
|
|
28
|
+
return @preloaded_value.keys if defined?(@preloaded_value)
|
|
29
|
+
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def values
|
|
34
|
+
@preload_context&.resolve!
|
|
35
|
+
return @preloaded_value.values if defined?(@preloaded_value)
|
|
36
|
+
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Redis
|
|
4
|
+
module Objects
|
|
5
|
+
module Preloadable
|
|
6
|
+
module TypePatches
|
|
7
|
+
module List
|
|
8
|
+
def preload!(raw_value)
|
|
9
|
+
@preloaded_value = raw_value || []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def value
|
|
13
|
+
@preload_context&.resolve!
|
|
14
|
+
return @preloaded_value if defined?(@preloaded_value)
|
|
15
|
+
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def values
|
|
20
|
+
value
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def [](index, length = nil)
|
|
24
|
+
@preload_context&.resolve!
|
|
25
|
+
if defined?(@preloaded_value)
|
|
26
|
+
return length ? @preloaded_value[index, length] : @preloaded_value[index]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def length
|
|
33
|
+
@preload_context&.resolve!
|
|
34
|
+
return @preloaded_value.length if defined?(@preloaded_value)
|
|
35
|
+
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def empty?
|
|
40
|
+
@preload_context&.resolve!
|
|
41
|
+
return @preloaded_value.empty? if defined?(@preloaded_value)
|
|
42
|
+
|
|
43
|
+
super
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Redis
|
|
4
|
+
module Objects
|
|
5
|
+
module Preloadable
|
|
6
|
+
module TypePatches
|
|
7
|
+
module Set
|
|
8
|
+
def preload!(raw_value)
|
|
9
|
+
@preloaded_value = raw_value || []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def members
|
|
13
|
+
@preload_context&.resolve!
|
|
14
|
+
return @preloaded_value if defined?(@preloaded_value)
|
|
15
|
+
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def include?(member)
|
|
20
|
+
@preload_context&.resolve!
|
|
21
|
+
return @preloaded_value.include?(member.to_s) if defined?(@preloaded_value)
|
|
22
|
+
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def length
|
|
27
|
+
@preload_context&.resolve!
|
|
28
|
+
return @preloaded_value.length if defined?(@preloaded_value)
|
|
29
|
+
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def empty?
|
|
34
|
+
@preload_context&.resolve!
|
|
35
|
+
return @preloaded_value.empty? if defined?(@preloaded_value)
|
|
36
|
+
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Redis
|
|
4
|
+
module Objects
|
|
5
|
+
module Preloadable
|
|
6
|
+
module TypePatches
|
|
7
|
+
module SortedSet
|
|
8
|
+
def preload!(raw_value)
|
|
9
|
+
@preloaded_value = raw_value || []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def members(options = {})
|
|
13
|
+
@preload_context&.resolve!
|
|
14
|
+
if defined?(@preloaded_value)
|
|
15
|
+
return @preloaded_value if options[:with_scores]
|
|
16
|
+
|
|
17
|
+
return @preloaded_value.map(&:first)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def score(member)
|
|
24
|
+
@preload_context&.resolve!
|
|
25
|
+
if defined?(@preloaded_value)
|
|
26
|
+
pair = @preloaded_value.find { |m, _| m == member.to_s }
|
|
27
|
+
return pair&.last
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def rank(member)
|
|
34
|
+
@preload_context&.resolve!
|
|
35
|
+
return @preloaded_value.index { |m, _| m == member.to_s } if defined?(@preloaded_value)
|
|
36
|
+
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def length
|
|
41
|
+
@preload_context&.resolve!
|
|
42
|
+
return @preloaded_value.length if defined?(@preloaded_value)
|
|
43
|
+
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Redis
|
|
4
|
+
module Objects
|
|
5
|
+
module Preloadable
|
|
6
|
+
module TypePatches
|
|
7
|
+
module Value
|
|
8
|
+
def preload!(raw_value)
|
|
9
|
+
@preloaded_value = raw_value
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def value
|
|
13
|
+
@preload_context&.resolve!
|
|
14
|
+
return @preloaded_value if defined?(@preloaded_value)
|
|
15
|
+
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def nil?
|
|
20
|
+
@preload_context&.resolve!
|
|
21
|
+
return @preloaded_value.nil? if defined?(@preloaded_value)
|
|
22
|
+
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require "redis-objects"
|
|
5
|
+
|
|
6
|
+
require_relative "preloadable/version"
|
|
7
|
+
require_relative "preloadable/type_patches/counter"
|
|
8
|
+
require_relative "preloadable/type_patches/value"
|
|
9
|
+
require_relative "preloadable/type_patches/list"
|
|
10
|
+
require_relative "preloadable/type_patches/set"
|
|
11
|
+
require_relative "preloadable/type_patches/sorted_set"
|
|
12
|
+
require_relative "preloadable/type_patches/hash_key"
|
|
13
|
+
require_relative "preloadable/preload_context"
|
|
14
|
+
require_relative "preloadable/relation_extension"
|
|
15
|
+
require_relative "preloadable/model_extension"
|
|
16
|
+
|
|
17
|
+
Redis::Counter.prepend(Redis::Objects::Preloadable::TypePatches::Counter)
|
|
18
|
+
Redis::Value.prepend(Redis::Objects::Preloadable::TypePatches::Value)
|
|
19
|
+
Redis::List.prepend(Redis::Objects::Preloadable::TypePatches::List)
|
|
20
|
+
Redis::Set.prepend(Redis::Objects::Preloadable::TypePatches::Set)
|
|
21
|
+
Redis::SortedSet.prepend(Redis::Objects::Preloadable::TypePatches::SortedSet)
|
|
22
|
+
Redis::HashKey.prepend(Redis::Objects::Preloadable::TypePatches::HashKey)
|
|
23
|
+
|
|
24
|
+
class Redis
|
|
25
|
+
module Objects
|
|
26
|
+
module Preloadable
|
|
27
|
+
def self.included(base)
|
|
28
|
+
base.include(ModelExtension)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.preload(records, *names)
|
|
32
|
+
records = records.to_a
|
|
33
|
+
return records if records.empty? || names.empty?
|
|
34
|
+
|
|
35
|
+
context = PreloadContext.new(records, names)
|
|
36
|
+
records.each do |record|
|
|
37
|
+
names.each do |name|
|
|
38
|
+
record.public_send(name).instance_variable_set(:@preload_context, context)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
records
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: redis-objects-preloadable
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- kyohah
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activerecord
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: redis-objects
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.7'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.7'
|
|
40
|
+
description: |
|
|
41
|
+
Provides batch loading (MGET / pipeline) for Redis::Objects attributes on
|
|
42
|
+
ActiveRecord models, following the same design as ActiveRecord's `preload`.
|
|
43
|
+
Supports counter, value, list, set, sorted_set, and hash_key types.
|
|
44
|
+
email:
|
|
45
|
+
- 3257272+kyohah@users.noreply.github.com
|
|
46
|
+
executables: []
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- CHANGELOG.md
|
|
51
|
+
- LICENSE.txt
|
|
52
|
+
- README.md
|
|
53
|
+
- lib/redis/objects/preloadable.rb
|
|
54
|
+
- lib/redis/objects/preloadable/model_extension.rb
|
|
55
|
+
- lib/redis/objects/preloadable/preload_context.rb
|
|
56
|
+
- lib/redis/objects/preloadable/relation_extension.rb
|
|
57
|
+
- lib/redis/objects/preloadable/type_patches/counter.rb
|
|
58
|
+
- lib/redis/objects/preloadable/type_patches/hash_key.rb
|
|
59
|
+
- lib/redis/objects/preloadable/type_patches/list.rb
|
|
60
|
+
- lib/redis/objects/preloadable/type_patches/set.rb
|
|
61
|
+
- lib/redis/objects/preloadable/type_patches/sorted_set.rb
|
|
62
|
+
- lib/redis/objects/preloadable/type_patches/value.rb
|
|
63
|
+
- lib/redis/objects/preloadable/version.rb
|
|
64
|
+
homepage: https://github.com/kyohah/redis-objects-preloadable
|
|
65
|
+
licenses:
|
|
66
|
+
- MIT
|
|
67
|
+
metadata:
|
|
68
|
+
homepage_uri: https://github.com/kyohah/redis-objects-preloadable
|
|
69
|
+
source_code_uri: https://github.com/kyohah/redis-objects-preloadable
|
|
70
|
+
changelog_uri: https://github.com/kyohah/redis-objects-preloadable/blob/main/CHANGELOG.md
|
|
71
|
+
rubygems_mfa_required: 'true'
|
|
72
|
+
rdoc_options: []
|
|
73
|
+
require_paths:
|
|
74
|
+
- lib
|
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - ">="
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: '3.1'
|
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: '0'
|
|
85
|
+
requirements: []
|
|
86
|
+
rubygems_version: 4.0.5
|
|
87
|
+
specification_version: 4
|
|
88
|
+
summary: Eliminate N+1 Redis calls for redis-objects in ActiveRecord models
|
|
89
|
+
test_files: []
|