redis-objects-preloadable 0.1.0 → 0.1.1
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 +4 -4
- data/CHANGELOG.md +4 -0
- data/lib/redis/objects/preloadable/model_extension.rb +22 -2
- data/lib/redis/objects/preloadable/preload_context.rb +21 -0
- data/lib/redis/objects/preloadable/relation_extension.rb +23 -0
- data/lib/redis/objects/preloadable/type_patches/counter.rb +3 -0
- data/lib/redis/objects/preloadable/type_patches/hash_key.rb +3 -0
- data/lib/redis/objects/preloadable/type_patches/list.rb +3 -0
- data/lib/redis/objects/preloadable/type_patches/set.rb +3 -0
- data/lib/redis/objects/preloadable/type_patches/sorted_set.rb +3 -0
- data/lib/redis/objects/preloadable/type_patches/value.rb +3 -0
- data/lib/redis/objects/preloadable/version.rb +1 -1
- data/lib/redis/objects/preloadable.rb +47 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2135bc0cffef091721bc51971f69adbfbe636ef10a372758db3e4de164bf7ec9
|
|
4
|
+
data.tar.gz: cae263e6dafa776af239cf3ce8d7a38d1f2eccbf2452b90e5a2a8afe82bf8680
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 82c717b269c84a0c977b195ec5be729a177e83d00268506eb9d2efc641cf1d32938fd4f9eb9c6346746b55e11ba0b41d954051719140c7e1f99ecd54e345f085
|
|
7
|
+
data.tar.gz: 4b96bf14d1cce588cf96500819898aa0da85468f75fce4e9b9042d6ea22bd50c25ab392def2120323e39d0b47e7a295204a674ea3af48a811e26a6e4c50706e9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.1] - 2026-02-22
|
|
4
|
+
|
|
5
|
+
- Fix Rails 8.1 / Ruby 3.4 compatibility: forward all arguments in `ModelExtension.all` override to avoid `ArgumentError` when `ActiveRecord::Persistence#_find_record` calls `all(all_queries: ...)` ([#fix](https://github.com/kyohah/redis-objects-preloadable/issues))
|
|
6
|
+
|
|
3
7
|
## [0.1.0] - 2026-02-20
|
|
4
8
|
|
|
5
9
|
- Initial release
|
|
@@ -3,17 +3,37 @@
|
|
|
3
3
|
class Redis
|
|
4
4
|
module Objects
|
|
5
5
|
module Preloadable
|
|
6
|
+
# ActiveSupport::Concern that integrates Preloadable into ActiveRecord models.
|
|
7
|
+
#
|
|
8
|
+
# Included automatically when a model does +include Redis::Objects::Preloadable+.
|
|
9
|
+
# Overrides +.all+ to extend relations with {RelationExtension} and provides
|
|
10
|
+
# the backward-compatible +read_redis_counter+ helper.
|
|
11
|
+
#
|
|
6
12
|
module ModelExtension
|
|
7
13
|
extend ActiveSupport::Concern
|
|
8
14
|
|
|
9
15
|
class_methods do
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
# @api private
|
|
17
|
+
def all(...)
|
|
18
|
+
super(...).extending(Redis::Objects::Preloadable::RelationExtension)
|
|
12
19
|
end
|
|
13
20
|
end
|
|
14
21
|
|
|
15
22
|
private
|
|
16
23
|
|
|
24
|
+
# Backward-compatible helper for reading a counter with SQL fallback.
|
|
25
|
+
#
|
|
26
|
+
# If the counter has a preloaded value, it is returned directly.
|
|
27
|
+
# Otherwise, checks Redis and falls back to the block (SQL query).
|
|
28
|
+
#
|
|
29
|
+
# With transparent preloading, this method is no longer necessary.
|
|
30
|
+
# You can access +counter.value+ directly instead.
|
|
31
|
+
#
|
|
32
|
+
# @param _name [Symbol] the counter attribute name (unused in transparent mode)
|
|
33
|
+
# @param counter [Redis::Counter] the counter instance
|
|
34
|
+
# @yield SQL fallback block that returns the count
|
|
35
|
+
# @return [Integer] the counter value
|
|
36
|
+
#
|
|
17
37
|
def read_redis_counter(_name, counter)
|
|
18
38
|
if counter.instance_variable_defined?(:@preloaded_value)
|
|
19
39
|
raw = counter.instance_variable_get(:@preloaded_value)
|
|
@@ -3,15 +3,36 @@
|
|
|
3
3
|
class Redis
|
|
4
4
|
module Objects
|
|
5
5
|
module Preloadable
|
|
6
|
+
# Redis::Objects types that support MGET (single-value keys).
|
|
6
7
|
MGET_TYPES = %i[counter value].freeze
|
|
7
8
|
|
|
9
|
+
# Holds a batch of records and attribute names, and resolves them
|
|
10
|
+
# in a single Redis round-trip on first access.
|
|
11
|
+
#
|
|
12
|
+
# PreloadContext is created by {RelationExtension#load} or
|
|
13
|
+
# {Preloadable.preload} and attached to each redis-objects instance.
|
|
14
|
+
# Resolution is lazy and idempotent.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# context = PreloadContext.new(records, [:view_count, :tag_ids])
|
|
18
|
+
# context.resolve! # fetches all values in one batch
|
|
19
|
+
# context.resolve! # no-op on subsequent calls
|
|
20
|
+
#
|
|
8
21
|
class PreloadContext
|
|
22
|
+
# @param records [Array<ActiveRecord::Base>] records to preload
|
|
23
|
+
# @param names [Array<Symbol>] redis-objects attribute names
|
|
9
24
|
def initialize(records, names)
|
|
10
25
|
@records = records
|
|
11
26
|
@names = names
|
|
12
27
|
@resolved = false
|
|
13
28
|
end
|
|
14
29
|
|
|
30
|
+
# Resolve all preloaded attributes in a single batch.
|
|
31
|
+
#
|
|
32
|
+
# Uses MGET for counter/value types and pipelined commands for
|
|
33
|
+
# list/set/sorted_set/hash_key types. This method is idempotent.
|
|
34
|
+
#
|
|
35
|
+
# @return [void]
|
|
15
36
|
def resolve!
|
|
16
37
|
return if @resolved
|
|
17
38
|
|
|
@@ -3,15 +3,38 @@
|
|
|
3
3
|
class Redis
|
|
4
4
|
module Objects
|
|
5
5
|
module Preloadable
|
|
6
|
+
# Extends ActiveRecord::Relation with +redis_preload+ scope.
|
|
7
|
+
#
|
|
8
|
+
# This module is automatically mixed into relations via
|
|
9
|
+
# {ModelExtension::ClassMethods#all}.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# Widget.where(active: true).redis_preload(:view_count, :tag_ids).each do |w|
|
|
13
|
+
# w.view_count.value # preloaded
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
6
16
|
module RelationExtension
|
|
17
|
+
# Declare redis-objects attributes to batch-preload when the relation loads.
|
|
18
|
+
#
|
|
19
|
+
# Can be chained with other ActiveRecord scopes. Preloading is lazy:
|
|
20
|
+
# no Redis calls until the first attribute access.
|
|
21
|
+
#
|
|
22
|
+
# @param names [Array<Symbol>] redis-objects attribute names
|
|
23
|
+
# @return [ActiveRecord::Relation] a new relation with preload metadata
|
|
24
|
+
#
|
|
25
|
+
# @example
|
|
26
|
+
# Widget.order(:id).redis_preload(:view_count).limit(100)
|
|
27
|
+
#
|
|
7
28
|
def redis_preload(*names)
|
|
8
29
|
spawn.tap { |r| r.instance_variable_set(:@redis_preload_names, names) }
|
|
9
30
|
end
|
|
10
31
|
|
|
32
|
+
# @return [Array<Symbol>] the redis-objects attribute names to preload
|
|
11
33
|
def redis_preload_names
|
|
12
34
|
@redis_preload_names || []
|
|
13
35
|
end
|
|
14
36
|
|
|
37
|
+
# @api private
|
|
15
38
|
def load
|
|
16
39
|
result = super
|
|
17
40
|
|
|
@@ -4,7 +4,10 @@ class Redis
|
|
|
4
4
|
module Objects
|
|
5
5
|
module Preloadable
|
|
6
6
|
module TypePatches
|
|
7
|
+
# Prepended onto Redis::Counter to support preloaded values.
|
|
8
|
+
# Fetched via MGET. Returns +.to_i+ (0 for nil/missing keys).
|
|
7
9
|
module Counter
|
|
10
|
+
# @api private
|
|
8
11
|
def preload!(raw_value)
|
|
9
12
|
@preloaded_value = raw_value
|
|
10
13
|
end
|
|
@@ -4,7 +4,10 @@ class Redis
|
|
|
4
4
|
module Objects
|
|
5
5
|
module Preloadable
|
|
6
6
|
module TypePatches
|
|
7
|
+
# Prepended onto Redis::HashKey to support preloaded values.
|
|
8
|
+
# Fetched via HGETALL in a pipeline.
|
|
7
9
|
module HashKey
|
|
10
|
+
# @api private
|
|
8
11
|
def preload!(raw_value)
|
|
9
12
|
@preloaded_value = raw_value || {}
|
|
10
13
|
end
|
|
@@ -4,7 +4,10 @@ class Redis
|
|
|
4
4
|
module Objects
|
|
5
5
|
module Preloadable
|
|
6
6
|
module TypePatches
|
|
7
|
+
# Prepended onto Redis::List to support preloaded values.
|
|
8
|
+
# Fetched via LRANGE 0 -1 in a pipeline.
|
|
7
9
|
module List
|
|
10
|
+
# @api private
|
|
8
11
|
def preload!(raw_value)
|
|
9
12
|
@preloaded_value = raw_value || []
|
|
10
13
|
end
|
|
@@ -4,7 +4,10 @@ class Redis
|
|
|
4
4
|
module Objects
|
|
5
5
|
module Preloadable
|
|
6
6
|
module TypePatches
|
|
7
|
+
# Prepended onto Redis::Set to support preloaded values.
|
|
8
|
+
# Fetched via SMEMBERS in a pipeline.
|
|
7
9
|
module Set
|
|
10
|
+
# @api private
|
|
8
11
|
def preload!(raw_value)
|
|
9
12
|
@preloaded_value = raw_value || []
|
|
10
13
|
end
|
|
@@ -4,7 +4,10 @@ class Redis
|
|
|
4
4
|
module Objects
|
|
5
5
|
module Preloadable
|
|
6
6
|
module TypePatches
|
|
7
|
+
# Prepended onto Redis::SortedSet to support preloaded values.
|
|
8
|
+
# Fetched via ZRANGE 0 -1 WITHSCORES in a pipeline.
|
|
7
9
|
module SortedSet
|
|
10
|
+
# @api private
|
|
8
11
|
def preload!(raw_value)
|
|
9
12
|
@preloaded_value = raw_value || []
|
|
10
13
|
end
|
|
@@ -4,7 +4,10 @@ class Redis
|
|
|
4
4
|
module Objects
|
|
5
5
|
module Preloadable
|
|
6
6
|
module TypePatches
|
|
7
|
+
# Prepended onto Redis::Value to support preloaded values.
|
|
8
|
+
# Fetched via MGET. Returns raw string or nil.
|
|
7
9
|
module Value
|
|
10
|
+
# @api private
|
|
8
11
|
def preload!(raw_value)
|
|
9
12
|
@preloaded_value = raw_value
|
|
10
13
|
end
|
|
@@ -21,6 +21,36 @@ Redis::Set.prepend(Redis::Objects::Preloadable::TypePatches::Set)
|
|
|
21
21
|
Redis::SortedSet.prepend(Redis::Objects::Preloadable::TypePatches::SortedSet)
|
|
22
22
|
Redis::HashKey.prepend(Redis::Objects::Preloadable::TypePatches::HashKey)
|
|
23
23
|
|
|
24
|
+
# Redis::Objects::Preloadable eliminates N+1 Redis calls for redis-objects
|
|
25
|
+
# attributes on ActiveRecord models.
|
|
26
|
+
#
|
|
27
|
+
# It provides two APIs:
|
|
28
|
+
#
|
|
29
|
+
# 1. +redis_preload+ scope on ActiveRecord relations
|
|
30
|
+
# 2. +Redis::Objects::Preloadable.preload+ for arbitrary record arrays
|
|
31
|
+
#
|
|
32
|
+
# == Basic usage
|
|
33
|
+
#
|
|
34
|
+
# class Pack < ApplicationRecord
|
|
35
|
+
# include Redis::Objects
|
|
36
|
+
# include Redis::Objects::Preloadable
|
|
37
|
+
#
|
|
38
|
+
# counter :cache_total_count
|
|
39
|
+
# list :recent_item_ids
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# # Scope-based (top-level queries)
|
|
43
|
+
# Pack.redis_preload(:cache_total_count, :recent_item_ids).limit(100).each do |pack|
|
|
44
|
+
# pack.cache_total_count.value # preloaded via MGET
|
|
45
|
+
# pack.recent_item_ids.values # preloaded via pipeline
|
|
46
|
+
# end
|
|
47
|
+
#
|
|
48
|
+
# == Association-loaded records
|
|
49
|
+
#
|
|
50
|
+
# users = User.includes(:articles).load
|
|
51
|
+
# articles = users.flat_map(&:articles)
|
|
52
|
+
# Redis::Objects::Preloadable.preload(articles, :view_count)
|
|
53
|
+
#
|
|
24
54
|
class Redis
|
|
25
55
|
module Objects
|
|
26
56
|
module Preloadable
|
|
@@ -28,6 +58,23 @@ class Redis
|
|
|
28
58
|
base.include(ModelExtension)
|
|
29
59
|
end
|
|
30
60
|
|
|
61
|
+
# Batch-preload Redis::Objects attributes on an array of records.
|
|
62
|
+
#
|
|
63
|
+
# Use this for records loaded outside of +redis_preload+ scope,
|
|
64
|
+
# such as association-loaded records via +includes+.
|
|
65
|
+
#
|
|
66
|
+
# Preloading is lazy: no Redis calls are made until the first
|
|
67
|
+
# attribute access on any of the records.
|
|
68
|
+
#
|
|
69
|
+
# @param records [Array<ActiveRecord::Base>, ActiveRecord::Relation] records to preload
|
|
70
|
+
# @param names [Array<Symbol>] redis-objects attribute names to preload
|
|
71
|
+
# @return [Array<ActiveRecord::Base>] the same records array
|
|
72
|
+
#
|
|
73
|
+
# @example Preload on association-loaded records
|
|
74
|
+
# users = User.includes(:articles).load
|
|
75
|
+
# articles = users.flat_map(&:articles)
|
|
76
|
+
# Redis::Objects::Preloadable.preload(articles, :view_count, :cached_summary)
|
|
77
|
+
#
|
|
31
78
|
def self.preload(records, *names)
|
|
32
79
|
records = records.to_a
|
|
33
80
|
return records if records.empty? || names.empty?
|