n1_loader 1.2.0 → 1.4.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 +21 -1
- data/README.md +35 -29
- data/lib/n1_loader/active_record/loader_collection.rb +1 -3
- data/lib/n1_loader/ar_lazy_preload/loadable.rb +1 -1
- data/lib/n1_loader/ar_lazy_preload/loader_collection_patch.rb +1 -1
- data/lib/n1_loader/core/loadable.rb +4 -4
- data/lib/n1_loader/core/loader.rb +69 -5
- data/lib/n1_loader/core/loader_collection.rb +4 -2
- data/lib/n1_loader/core/preloader.rb +12 -2
- data/lib/n1_loader/version.rb +1 -1
- data/lib/n1_loader.rb +2 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fc06934b6c230d9b48dce18c7e9ee49afad4ab32d51d4550eb3e2916fe977656
|
4
|
+
data.tar.gz: 456575999736bba2b4b8f33ff02a4908a7705b2cc31875bca9c80f3dbeeb4125
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4640cffcc0fe6e03672ba46fc16e9fc29fc33d1c18b6d9e54e4c6026cd0d0b465846a60e0b75ab0dec46fd4db21ae914a0f92d3cc4623e622b4782094d6ce680
|
7
|
+
data.tar.gz: a010ffac1ce7604ce9434de734fa20f0e5162ff7307f6ddcafe0b9be1119a4f37d3774b7dd4beb8d36affd56e4f30830d01904406ce1f2b2fc2989d7f9910fa1
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,24 @@
|
|
1
|
-
## [1.
|
1
|
+
## [1.4.1] - 2022-02-24
|
2
|
+
|
3
|
+
- Fix preloading of invalid objects
|
4
|
+
|
5
|
+
## [1.4.0] - 2022-02-22
|
6
|
+
|
7
|
+
- add support of optional arguments
|
8
|
+
|
9
|
+
BREAKING CHANGES:
|
10
|
+
- rework arguments to use single definition through `argument <name>` only
|
11
|
+
- use keyword arguments
|
12
|
+
|
13
|
+
## [1.3.0] - 2022-02-22
|
14
|
+
|
15
|
+
- add support of named arguments with `argument <name>`
|
16
|
+
|
17
|
+
BREAKING CHANGES:
|
18
|
+
- rename `n1_load` to `n1_optimized`
|
19
|
+
- rework `def self.arguments_key` to `cache_key`
|
20
|
+
|
21
|
+
## [1.2.0] - 2022-01-14
|
2
22
|
|
3
23
|
- Introduce arguments support.
|
4
24
|
|
data/README.md
CHANGED
@@ -45,7 +45,7 @@ class User
|
|
45
45
|
include N1Loader::Loadable
|
46
46
|
|
47
47
|
# with inline loader
|
48
|
-
|
48
|
+
n1_optimized :orders_count do |users|
|
49
49
|
orders_per_user = Order.where(user: users).group(:user_id).count
|
50
50
|
|
51
51
|
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
@@ -69,7 +69,7 @@ class User
|
|
69
69
|
include N1Loader::Loadable
|
70
70
|
|
71
71
|
# with inline loader
|
72
|
-
|
72
|
+
n1_optimized :orders_count do |users|
|
73
73
|
orders_per_user = Order.where(user: users).group(:user_id).count
|
74
74
|
|
75
75
|
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
@@ -98,14 +98,14 @@ end
|
|
98
98
|
|
99
99
|
class User
|
100
100
|
include N1Loader::Loadable
|
101
|
-
|
102
|
-
|
101
|
+
|
102
|
+
n1_optimized :orders_count, OrdersCountLoader
|
103
103
|
end
|
104
104
|
|
105
105
|
class Customer
|
106
106
|
include N1Loader::Loadable
|
107
|
-
|
108
|
-
|
107
|
+
|
108
|
+
n1_optimized :orders_count, OrdersCountLoader
|
109
109
|
end
|
110
110
|
|
111
111
|
User.new.orders_count # => works
|
@@ -119,7 +119,7 @@ class User
|
|
119
119
|
include N1Loader::Loadable
|
120
120
|
|
121
121
|
# with inline loader
|
122
|
-
|
122
|
+
n1_optimized :orders_count do |users|
|
123
123
|
orders_per_user = Order.where(user: users).group(:user_id).count
|
124
124
|
|
125
125
|
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
@@ -159,8 +159,8 @@ end
|
|
159
159
|
```ruby
|
160
160
|
class User
|
161
161
|
include N1Loader::Loadable
|
162
|
-
|
163
|
-
|
162
|
+
|
163
|
+
n1_optimized :orders_count do # no arguments passed to the block, so we can override both perform and single.
|
164
164
|
def perform(users)
|
165
165
|
orders_per_user = Order.where(user: users).group(:user_id).count
|
166
166
|
|
@@ -188,21 +188,25 @@ users.map(&:orders_count) # perform will be used once without N+1
|
|
188
188
|
class User
|
189
189
|
include N1Loader::Loadable
|
190
190
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
191
|
+
n1_optimized :orders_count do
|
192
|
+
argument :type
|
193
|
+
|
194
|
+
def perform(users)
|
195
|
+
orders_per_user = Order.where(type: type, user: users).group(:user_id).count
|
196
|
+
|
197
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
198
|
+
end
|
195
199
|
end
|
196
200
|
end
|
197
201
|
|
198
202
|
user = User.new
|
199
|
-
user.orders_count(:gifts) # The loader will be performed first time for this argument
|
200
|
-
user.orders_count(:sales) # The loader will be performed first time for this argument
|
201
|
-
user.orders_count(:gifts) # The cached value will be used
|
203
|
+
user.orders_count(type: :gifts) # The loader will be performed first time for this argument
|
204
|
+
user.orders_count(type: :sales) # The loader will be performed first time for this argument
|
205
|
+
user.orders_count(type: :gifts) # The cached value will be used
|
202
206
|
|
203
207
|
users = [User.new, User.new]
|
204
208
|
N1Loader::Preloader.new(users).preload(:orders_count)
|
205
|
-
users.map { |user| user.orders_count(:gifts) } # No N+1 here
|
209
|
+
users.map { |user| user.orders_count(type: :gifts) } # No N+1 here
|
206
210
|
```
|
207
211
|
|
208
212
|
_Note_: By default, we use `arguments.map(&:object_id)` to identify arguments but in some cases,
|
@@ -212,22 +216,22 @@ you may want to override it, for example:
|
|
212
216
|
class User
|
213
217
|
include N1Loader::Loadable
|
214
218
|
|
215
|
-
|
216
|
-
|
219
|
+
n1_optimized :orders_count do
|
220
|
+
argument :sale
|
221
|
+
|
222
|
+
cache_key { sale.id }
|
223
|
+
|
224
|
+
def perform(users)
|
217
225
|
orders_per_user = Order.where(sale: sale, user: users).group(:user_id).count
|
218
226
|
|
219
227
|
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
220
228
|
end
|
221
|
-
|
222
|
-
def self.arguments_key(sale)
|
223
|
-
sale.id
|
224
|
-
end
|
225
229
|
end
|
226
230
|
end
|
227
231
|
|
228
232
|
user = User.new
|
229
|
-
user.orders_count(Sale.first) # perform will be executed and value will be cached
|
230
|
-
user.orders_count(Sale.first) # the cached value will be returned
|
233
|
+
user.orders_count(sale: Sale.first) # perform will be executed and value will be cached
|
234
|
+
user.orders_count(sale: Sale.first) # the cached value will be returned
|
231
235
|
```
|
232
236
|
|
233
237
|
|
@@ -235,11 +239,13 @@ user.orders_count(Sale.first) # the cached value will be returned
|
|
235
239
|
|
236
240
|
### [ActiveRecord][5]
|
237
241
|
|
242
|
+
_Note_: Rails 7 support is coming soon! Stay tuned!
|
243
|
+
|
238
244
|
```ruby
|
239
245
|
class User < ActiveRecord::Base
|
240
246
|
include N1Loader::Loadable
|
241
|
-
|
242
|
-
|
247
|
+
|
248
|
+
n1_optimized :orders_count do |users|
|
243
249
|
orders_per_user = Order.where(user: users).group(:user_id).count
|
244
250
|
|
245
251
|
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
@@ -266,8 +272,8 @@ users.map(&:orders_count)
|
|
266
272
|
```ruby
|
267
273
|
class User < ActiveRecord::Base
|
268
274
|
include N1Loader::Loadable
|
269
|
-
|
270
|
-
|
275
|
+
|
276
|
+
n1_optimized :orders_count do |users|
|
271
277
|
orders_per_user = Order.where(user: users).group(:user_id).count
|
272
278
|
|
273
279
|
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
@@ -1,9 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
N1Loader::LoaderCollection.define_method :preloaded_records do
|
4
|
-
|
5
|
-
raise N1Loader::ActiveRecord::InvalidPreloading, "Cannot preload loader with arguments"
|
6
|
-
end
|
4
|
+
raise N1Loader::ActiveRecord::InvalidPreloading, "Cannot preload loader with arguments" if loader_class.arguments
|
7
5
|
|
8
6
|
with.preloaded_records
|
9
7
|
end
|
@@ -45,9 +45,9 @@ module N1Loader
|
|
45
45
|
respond_to?("#{name}_loader")
|
46
46
|
end
|
47
47
|
|
48
|
-
def
|
48
|
+
def n1_optimized(name, loader = nil, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
49
49
|
loader ||= Class.new(N1Loader::Loader) do
|
50
|
-
if block
|
50
|
+
if block.arity == 1
|
51
51
|
define_method(:perform, &block)
|
52
52
|
else
|
53
53
|
class_eval(&block)
|
@@ -73,10 +73,10 @@ module N1Loader
|
|
73
73
|
instance_variable_get(loader_variable_name) || send("#{loader_name}_reload")
|
74
74
|
end
|
75
75
|
|
76
|
-
define_method(name) do
|
76
|
+
define_method(name) do |reload: false, **args|
|
77
77
|
send("#{loader_name}_reload") if reload
|
78
78
|
|
79
|
-
send(loader_name).with(
|
79
|
+
send(loader_name).with(**args).for(self)
|
80
80
|
end
|
81
81
|
|
82
82
|
[name, loader_name, loader_variable_name]
|
@@ -6,11 +6,35 @@ module N1Loader
|
|
6
6
|
# Subclasses must define +perform+ method that accepts single argument
|
7
7
|
# and returns hash where key is the element and value is what we want to load.
|
8
8
|
class Loader
|
9
|
-
|
10
|
-
|
9
|
+
class << self
|
10
|
+
attr_reader :arguments
|
11
|
+
|
12
|
+
# Defines an argument that can be accessed within the loader.
|
13
|
+
#
|
14
|
+
# First defined argument will have the value of first passed argument,
|
15
|
+
# meaning the order is important.
|
16
|
+
#
|
17
|
+
# @param name [Symbol]
|
18
|
+
# @param opts [Hash]
|
19
|
+
# @option opts [Boolean] optional false by default
|
20
|
+
def argument(name, **opts)
|
21
|
+
@arguments ||= []
|
22
|
+
|
23
|
+
define_method(name) { args[name] }
|
24
|
+
|
25
|
+
@arguments << opts.merge(name: name)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Defines a custom cache key that is calculated for passed arguments.
|
29
|
+
def cache_key(&block)
|
30
|
+
define_method(:cache_key) do
|
31
|
+
check_arguments!
|
32
|
+
instance_exec(&block)
|
33
|
+
end
|
34
|
+
end
|
11
35
|
end
|
12
36
|
|
13
|
-
def initialize(elements,
|
37
|
+
def initialize(elements, **args)
|
14
38
|
@elements = elements
|
15
39
|
@args = args
|
16
40
|
end
|
@@ -24,10 +48,48 @@ module N1Loader
|
|
24
48
|
loaded[element]
|
25
49
|
end
|
26
50
|
|
51
|
+
def cache_key
|
52
|
+
check_arguments!
|
53
|
+
args.values.map(&:object_id)
|
54
|
+
end
|
55
|
+
|
27
56
|
private
|
28
57
|
|
29
58
|
attr_reader :elements, :args
|
30
59
|
|
60
|
+
def check_missing_arguments! # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
61
|
+
return unless (arguments = self.class.arguments)
|
62
|
+
|
63
|
+
min = arguments.count { |argument| !argument[:optional] }
|
64
|
+
max = arguments.count
|
65
|
+
|
66
|
+
return if args.size >= min && args.size <= max
|
67
|
+
|
68
|
+
str =
|
69
|
+
if min == max
|
70
|
+
max.to_s
|
71
|
+
else
|
72
|
+
"#{min}..#{max}"
|
73
|
+
end
|
74
|
+
|
75
|
+
raise MissingArgument, "Loader requires #{str} arguments but #{args.size} were given"
|
76
|
+
end
|
77
|
+
|
78
|
+
def check_arguments!
|
79
|
+
check_missing_arguments!
|
80
|
+
check_invalid_arguments!
|
81
|
+
end
|
82
|
+
|
83
|
+
def check_invalid_arguments!
|
84
|
+
return unless (arguments = self.class.arguments)
|
85
|
+
|
86
|
+
args.each_key do |arg|
|
87
|
+
next if arguments.find { |argument| argument[:name] == arg }
|
88
|
+
|
89
|
+
raise InvalidArgument, "Loader doesn't define #{arg} argument"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
31
93
|
def perform(_elements)
|
32
94
|
raise NotImplemented, "Subclasses have to implement the method"
|
33
95
|
end
|
@@ -39,12 +101,14 @@ module N1Loader
|
|
39
101
|
def loaded
|
40
102
|
return @loaded if @loaded
|
41
103
|
|
104
|
+
check_arguments!
|
105
|
+
|
42
106
|
@loaded = {}.compare_by_identity
|
43
107
|
|
44
108
|
if elements.size == 1 && respond_to?(:single)
|
45
|
-
fulfill(elements.first, single(elements.first
|
109
|
+
fulfill(elements.first, single(elements.first))
|
46
110
|
elsif elements.any?
|
47
|
-
perform(elements
|
111
|
+
perform(elements)
|
48
112
|
end
|
49
113
|
|
50
114
|
@loaded
|
@@ -10,8 +10,10 @@ module N1Loader
|
|
10
10
|
@elements = elements
|
11
11
|
end
|
12
12
|
|
13
|
-
def with(
|
14
|
-
|
13
|
+
def with(**args)
|
14
|
+
loader = loader_class.new(elements, **args)
|
15
|
+
|
16
|
+
loaders[loader.cache_key] ||= loader
|
15
17
|
end
|
16
18
|
|
17
19
|
private
|
@@ -17,13 +17,23 @@ module N1Loader
|
|
17
17
|
def preload(*keys)
|
18
18
|
keys.flatten(1).flat_map do |key|
|
19
19
|
elements
|
20
|
-
.group_by { |element| element
|
20
|
+
.group_by { |element| loader_class(element, key) }
|
21
21
|
.map do |loader_class, grouped_elements|
|
22
|
+
next unless loader_class
|
23
|
+
|
22
24
|
loader_collection = N1Loader::LoaderCollection.new(loader_class, grouped_elements)
|
23
25
|
grouped_elements.each { |grouped_element| grouped_element.n1_loader_set(key, loader_collection) }
|
24
26
|
loader_collection
|
25
|
-
end
|
27
|
+
end.compact
|
26
28
|
end
|
27
29
|
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def loader_class(element, key)
|
34
|
+
element.class.respond_to?(:n1_loader_defined?) &&
|
35
|
+
element.class.n1_loader_defined?(key) &&
|
36
|
+
element.class.n1_loader(key)
|
37
|
+
end
|
28
38
|
end
|
29
39
|
end
|
data/lib/n1_loader/version.rb
CHANGED
data/lib/n1_loader.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: n1_loader
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Evgeniy Demin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-02-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|