n1_loader 1.0.0 → 1.4.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 +4 -4
- data/.circleci/config.yml +53 -18
- data/CHANGELOG.md +24 -0
- data/Gemfile +9 -0
- data/README.md +130 -96
- data/{gemfiles → activerecord-gemfiles}/ar_5_latest.gemfile +0 -4
- data/{gemfiles → activerecord-gemfiles}/ar_6_latest.gemfile +0 -4
- data/ar_lazy_preload-gemfiles/ar_lazy_preload_0.6.1.gemfile +3 -0
- data/ar_lazy_preload-gemfiles/ar_lazy_preload_master.gemfile +3 -0
- data/lib/n1_loader/active_record/loader.rb +5 -0
- data/lib/n1_loader/active_record/loader_collection.rb +7 -0
- data/lib/n1_loader/active_record.rb +12 -0
- data/lib/n1_loader/ar_lazy_preload/associated_context_builder.rb +8 -1
- data/lib/n1_loader/ar_lazy_preload/context_adapter.rb +8 -5
- data/lib/n1_loader/ar_lazy_preload/loadable.rb +3 -3
- data/lib/n1_loader/ar_lazy_preload/loader_collection_patch.rb +18 -0
- data/lib/n1_loader/ar_lazy_preload/loader_patch.rb +20 -0
- data/lib/n1_loader/ar_lazy_preload/preloader_patch.rb +23 -0
- data/lib/n1_loader/ar_lazy_preload.rb +9 -0
- data/lib/n1_loader/core/loadable.rb +14 -16
- data/lib/n1_loader/core/loader.rb +94 -16
- data/lib/n1_loader/core/loader_collection.rb +25 -0
- data/lib/n1_loader/core/preloader.rb +3 -3
- data/lib/n1_loader/version.rb +1 -1
- data/lib/n1_loader.rb +4 -0
- data/n1_loader.gemspec +2 -2
- metadata +17 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2d0f96166ae91c856012ea26c933cc56c7d33f68065b753a18adc3c6b9da7e87
|
4
|
+
data.tar.gz: adccc88c382d9283c2af9b41059298fa15563e47615678ef20629a0183a51253
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ad3b1273627cf7abb5922cc7139c34e5e9543dc91d296d7e44deea690367d32082dd1ddc028ba40a4ed7fa402a65eb8f5aed36b6d8df0436937e59aaa6cef65a
|
7
|
+
data.tar.gz: fde03e325f2b780a070786f1648a98d278933c84b3266cedf4db50cd268b029839ff06290416846a9ab2a85a25a0795c30684845e4da15909417389dbd1a7a82
|
data/.circleci/config.yml
CHANGED
@@ -11,8 +11,8 @@ executors:
|
|
11
11
|
docker:
|
12
12
|
- image: circleci/ruby:<< parameters.tag >>
|
13
13
|
environment:
|
14
|
-
|
15
|
-
|
14
|
+
BUNDLE_JOBS: 4
|
15
|
+
BUNDLE_RETRY: 3
|
16
16
|
working_directory: ~/n1_loader
|
17
17
|
|
18
18
|
jobs:
|
@@ -29,28 +29,49 @@ jobs:
|
|
29
29
|
parameters:
|
30
30
|
ruby-version:
|
31
31
|
type: string
|
32
|
-
gemfile:
|
32
|
+
activerecord-gemfile:
|
33
33
|
type: string
|
34
|
+
ar_lazy_preload-gemfile:
|
35
|
+
type: string
|
36
|
+
environment:
|
37
|
+
ACTIVERECORD_GEMFILE: << parameters.activerecord-gemfile >>
|
38
|
+
AR_LAZY_PRELOAD_GEMFILE: << parameters.ar_lazy_preload-gemfile >>
|
34
39
|
executor:
|
35
40
|
name: ruby
|
36
41
|
tag: << parameters.ruby-version >>
|
37
42
|
steps:
|
38
43
|
- attach_workspace:
|
39
44
|
at: ~/n1_loader
|
40
|
-
- run:
|
41
|
-
name: Use << parameters.gemfile >> as the Gemfile
|
42
|
-
command: bundle config --global gemfile << parameters.gemfile >>
|
43
45
|
- run:
|
44
46
|
name: Install the gems specified by the Gemfile
|
45
47
|
command: bundle install
|
46
48
|
- run:
|
47
|
-
name: Run RSpec
|
49
|
+
name: Run Core RSpec
|
48
50
|
command: |
|
49
51
|
bundle exec rspec --profile 10 \
|
50
52
|
--format RspecJunitFormatter \
|
51
|
-
--out test_results/
|
53
|
+
--out test_results/core.xml \
|
52
54
|
--format progress \
|
53
|
-
|
55
|
+
spec/n1_loader_spec.rb
|
56
|
+
- run:
|
57
|
+
name: Run ActiveRecord integration RSpec
|
58
|
+
command: |
|
59
|
+
bundle exec rspec --profile 10 \
|
60
|
+
--format RspecJunitFormatter \
|
61
|
+
--out test_results/activerecord-integration.xml \
|
62
|
+
--format progress \
|
63
|
+
spec/n1_loader_spec.rb \
|
64
|
+
spec/activerecord_spec.rb
|
65
|
+
- run:
|
66
|
+
name: Run ActiveRecord integration RSpec
|
67
|
+
command: |
|
68
|
+
bundle exec rspec --profile 10 \
|
69
|
+
--format RspecJunitFormatter \
|
70
|
+
--out test_results/ar-lazy-preload-integration.xml \
|
71
|
+
--format progress \
|
72
|
+
spec/n1_loader_spec.rb \
|
73
|
+
spec/activerecord_spec.rb \
|
74
|
+
spec/ar_lazy_preload_spec.rb
|
54
75
|
- store_test_results:
|
55
76
|
path: test_results
|
56
77
|
|
@@ -80,22 +101,36 @@ workflows:
|
|
80
101
|
parameters:
|
81
102
|
ruby-version: [
|
82
103
|
"2.5",
|
83
|
-
"2.6",
|
84
104
|
"2.7",
|
85
|
-
"3.0",
|
86
105
|
"latest"
|
87
106
|
]
|
88
|
-
gemfile: [
|
89
|
-
"
|
90
|
-
"
|
107
|
+
activerecord-gemfile: [
|
108
|
+
"ar_5_latest",
|
109
|
+
"ar_6_latest"
|
110
|
+
]
|
111
|
+
ar_lazy_preload-gemfile: [
|
112
|
+
"ar_lazy_preload_0.6.1",
|
113
|
+
"ar_lazy_preload_master"
|
91
114
|
]
|
92
115
|
exclude:
|
93
|
-
|
94
|
-
|
116
|
+
# Ruby 2.5 and AR Lazy Preload 1+
|
117
|
+
- ruby-version: "2.5"
|
118
|
+
activerecord-gemfile: "ar_5_latest"
|
119
|
+
ar_lazy_preload-gemfile: "ar_lazy_preload_master"
|
120
|
+
|
121
|
+
- ruby-version: "2.5"
|
122
|
+
activerecord-gemfile: "ar_6_latest"
|
123
|
+
ar_lazy_preload-gemfile: "ar_lazy_preload_master"
|
124
|
+
|
125
|
+
# AR 5 and ruby 3+
|
126
|
+
- ruby-version: "latest"
|
127
|
+
activerecord-gemfile: "ar_5_latest"
|
128
|
+
ar_lazy_preload-gemfile: "ar_lazy_preload_0.6.1"
|
95
129
|
- ruby-version: "latest"
|
96
|
-
gemfile: "
|
130
|
+
activerecord-gemfile: "ar_5_latest"
|
131
|
+
ar_lazy_preload-gemfile: "ar_lazy_preload_master"
|
97
132
|
|
98
|
-
name:
|
133
|
+
name: ruby-<< matrix.ruby-version >>-<< matrix.activerecord-gemfile >>-<< matrix.ar_lazy_preload-gemfile >>
|
99
134
|
- rubocop:
|
100
135
|
requires:
|
101
136
|
- checkout
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,27 @@
|
|
1
|
+
## [1.4.0] - 2022-02-22
|
2
|
+
|
3
|
+
- add support of optional arguments
|
4
|
+
|
5
|
+
BREAKING CHANGES:
|
6
|
+
- rework arguments to use single definition through `argument <name>` only
|
7
|
+
- use keyword arguments
|
8
|
+
|
9
|
+
## [1.3.0] - 2022-02-22
|
10
|
+
|
11
|
+
- add support of named arguments with `argument <name>`
|
12
|
+
|
13
|
+
BREAKING CHANGES:
|
14
|
+
- rename `n1_load` to `n1_optimized`
|
15
|
+
- rework `def self.arguments_key` to `cache_key`
|
16
|
+
|
17
|
+
## [1.2.0] - 2022-01-14
|
18
|
+
|
19
|
+
- Introduce arguments support.
|
20
|
+
|
21
|
+
## [1.1.0] - 2021-12-27
|
22
|
+
|
23
|
+
- Introduce `fulfill` method to abstract the storage.
|
24
|
+
|
1
25
|
## [1.0.0] - 2021-12-26
|
2
26
|
|
3
27
|
- Various of great features.
|
data/Gemfile
CHANGED
@@ -4,3 +4,12 @@ source "https://rubygems.org"
|
|
4
4
|
|
5
5
|
# Specify your gem's dependencies in n1_loader.gemspec
|
6
6
|
gemspec
|
7
|
+
|
8
|
+
# Hack to make Github work with Circle CI job names with slashes
|
9
|
+
gemfiles = []
|
10
|
+
gemfiles << "activerecord-gemfiles/#{ENV["ACTIVERECORD_GEMFILE"]}.gemfile" if ENV["ACTIVERECORD_GEMFILE"]
|
11
|
+
gemfiles << "ar_lazy_preload-gemfiles/#{ENV["AR_LAZY_PRELOAD_GEMFILE"]}.gemfile" if ENV["AR_LAZY_PRELOAD_GEMFILE"]
|
12
|
+
|
13
|
+
gemfiles.each do |path|
|
14
|
+
eval(File.read(path)) # rubocop:disable Security/Eval
|
15
|
+
end
|
data/README.md
CHANGED
@@ -14,6 +14,7 @@ It has many benefits:
|
|
14
14
|
- it supports [shareable loaders](#shareable-loaders) between multiple classes
|
15
15
|
- it supports [reloading](#reloading)
|
16
16
|
- it supports optimized [single object loading](#optimized-single-case)
|
17
|
+
- it supports [arguments](#arguments)
|
17
18
|
- it has an integration with [ActiveRecord][5] which makes it brilliant ([example](#activerecord))
|
18
19
|
- it has an integration with [ArLazyPreload][6] which makes it excellent ([example](#arlazypreload))
|
19
20
|
|
@@ -39,125 +40,115 @@ gem 'n1_loader', require: 'n1_loader/ar_lazy_preload'
|
|
39
40
|
|
40
41
|
## Usage
|
41
42
|
|
42
|
-
**Supported Ruby version:** 2.5, 2.6, 2.7, 3.0, and latest.
|
43
|
-
|
44
43
|
```ruby
|
45
|
-
class
|
44
|
+
class User
|
46
45
|
include N1Loader::Loadable
|
47
46
|
|
48
47
|
# with inline loader
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
# with custom loader
|
55
|
-
n1_loader :something, MyLoader
|
56
|
-
end
|
57
|
-
|
58
|
-
# Custom loader that can be shared with many classes
|
59
|
-
class MyLoader < N1Loader::Loader
|
60
|
-
# Has to return a hash that has keys as element from elements
|
61
|
-
def perform(elements)
|
62
|
-
elements.group_by(&:itself)
|
48
|
+
n1_optimized :orders_count do |users|
|
49
|
+
orders_per_user = Order.where(user: users).group(:user_id).count
|
50
|
+
|
51
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
63
52
|
end
|
64
53
|
end
|
65
54
|
|
66
55
|
# For single object
|
67
|
-
|
68
|
-
|
56
|
+
user = User.new
|
57
|
+
user.orders_count
|
69
58
|
|
70
59
|
# For multiple objects without N+1
|
71
|
-
|
72
|
-
N1Loader::Preloader.new(
|
73
|
-
|
60
|
+
users = [User.new, User.new]
|
61
|
+
N1Loader::Preloader.new(users).preload(:orders_count)
|
62
|
+
users.map(&:orders_count)
|
74
63
|
```
|
75
64
|
|
76
65
|
### Lazy loading
|
77
66
|
|
78
67
|
```ruby
|
79
|
-
class
|
68
|
+
class User
|
80
69
|
include N1Loader::Loadable
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
70
|
+
|
71
|
+
# with inline loader
|
72
|
+
n1_optimized :orders_count do |users|
|
73
|
+
orders_per_user = Order.where(user: users).group(:user_id).count
|
74
|
+
|
75
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
85
76
|
end
|
86
77
|
end
|
87
78
|
|
88
|
-
|
89
|
-
|
79
|
+
user = User.new # => nothing was done for loading
|
80
|
+
user.orders_count # => first time loading
|
90
81
|
|
91
|
-
|
92
|
-
N1Loader::Preloader.new([
|
93
|
-
|
82
|
+
users = [User.new, User.new] # => nothing was done for loading
|
83
|
+
N1Loader::Preloader.new([users]).preload(:orders_count) # => we only initialized loader but didn't perform it yet
|
84
|
+
users.map(&:orders_count) # => loading has happen for the first time (without N+1)
|
94
85
|
```
|
95
86
|
|
96
87
|
|
97
88
|
### Shareable loaders
|
98
89
|
|
99
90
|
```ruby
|
100
|
-
class
|
101
|
-
def perform(
|
102
|
-
|
103
|
-
|
91
|
+
class OrdersCountLoader < N1Loader::Loader
|
92
|
+
def perform(users)
|
93
|
+
orders_per_user = Order.where(user: users).group(:user_id).count
|
94
|
+
|
95
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
104
96
|
end
|
105
97
|
end
|
106
98
|
|
107
|
-
class
|
99
|
+
class User
|
108
100
|
include N1Loader::Loadable
|
109
|
-
|
110
|
-
|
101
|
+
|
102
|
+
n1_optimized :orders_count, OrdersCountLoader
|
111
103
|
end
|
112
104
|
|
113
|
-
class
|
105
|
+
class Customer
|
114
106
|
include N1Loader::Loadable
|
115
|
-
|
116
|
-
|
107
|
+
|
108
|
+
n1_optimized :orders_count, OrdersCountLoader
|
117
109
|
end
|
118
110
|
|
119
|
-
|
120
|
-
|
111
|
+
User.new.orders_count # => works
|
112
|
+
Customer.new.orders_count # => works
|
121
113
|
```
|
122
114
|
|
123
115
|
### Reloading
|
124
116
|
|
125
117
|
```ruby
|
126
|
-
class
|
118
|
+
class User
|
127
119
|
include N1Loader::Loadable
|
128
120
|
|
129
121
|
# with inline loader
|
130
|
-
|
131
|
-
|
132
|
-
|
122
|
+
n1_optimized :orders_count do |users|
|
123
|
+
orders_per_user = Order.where(user: users).group(:user_id).count
|
124
|
+
|
125
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
133
126
|
end
|
134
127
|
end
|
135
128
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
objects = [Example.new, Example.new]
|
129
|
+
user = User.new
|
130
|
+
user.orders_count # => loader is executed first time and value was cached
|
131
|
+
user.orders_count(reload: true) # => loader is executed again and a new value was cached
|
141
132
|
|
142
|
-
|
143
|
-
|
133
|
+
users = [User.new, User.new]
|
134
|
+
N1Loader::Preloader.new(users).preload(:orders_count) # => loader was initialized but not yet executed
|
135
|
+
users.map(&:orders_count) # => loader was executed first time without N+1 issue and values were cached
|
144
136
|
|
145
|
-
N1Loader::Preloader.new(
|
146
|
-
|
137
|
+
N1Loader::Preloader.new(users).preload(:orders_count) # => loader was initialized again but not yet executed
|
138
|
+
users.map(&:orders_count) # => new loader was executed first time without N+1 issue and new values were cached
|
147
139
|
```
|
148
140
|
|
149
141
|
### Isolated loaders
|
150
142
|
|
151
143
|
```ruby
|
152
|
-
class
|
144
|
+
class IsolatedLoader < N1Loader::Loader
|
153
145
|
def perform(elements)
|
154
|
-
|
155
|
-
elements.group_by(&:itself)
|
146
|
+
elements.each { |element| fulfill(element, [element]) }
|
156
147
|
end
|
157
148
|
end
|
158
149
|
|
159
150
|
objects = [1, 2, 3, 4]
|
160
|
-
loader =
|
151
|
+
loader = IsolatedLoader.new(objects)
|
161
152
|
objects.each do |object|
|
162
153
|
loader.for(object) # => it has no N+1 and it doesn't require to be injected in the class
|
163
154
|
end
|
@@ -166,48 +157,98 @@ end
|
|
166
157
|
### Optimized single case
|
167
158
|
|
168
159
|
```ruby
|
169
|
-
class
|
160
|
+
class User
|
170
161
|
include N1Loader::Loadable
|
171
|
-
|
172
|
-
|
173
|
-
def perform(
|
174
|
-
|
175
|
-
|
162
|
+
|
163
|
+
n1_optimized :orders_count do # no arguments passed to the block, so we can override both perform and single.
|
164
|
+
def perform(users)
|
165
|
+
orders_per_user = Order.where(user: users).group(:user_id).count
|
166
|
+
|
167
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
176
168
|
end
|
177
169
|
|
178
170
|
# Optimized for single object loading
|
179
|
-
def single(
|
180
|
-
|
181
|
-
|
171
|
+
def single(user)
|
172
|
+
user.orders.count
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
user = User.new
|
178
|
+
user.orders_count # single will be used here
|
179
|
+
|
180
|
+
users = [User.new, User.new]
|
181
|
+
N1Loader::Preloader.new(users).preload(:orders_count)
|
182
|
+
users.map(&:orders_count) # perform will be used once without N+1
|
183
|
+
```
|
184
|
+
|
185
|
+
### Arguments
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
class User
|
189
|
+
include N1Loader::Loadable
|
190
|
+
|
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]) }
|
182
198
|
end
|
183
199
|
end
|
184
200
|
end
|
185
201
|
|
186
|
-
|
187
|
-
|
202
|
+
user = User.new
|
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
|
188
206
|
|
189
|
-
|
190
|
-
N1Loader::Preloader.new(
|
191
|
-
|
207
|
+
users = [User.new, User.new]
|
208
|
+
N1Loader::Preloader.new(users).preload(:orders_count)
|
209
|
+
users.map { |user| user.orders_count(type: :gifts) } # No N+1 here
|
192
210
|
```
|
193
211
|
|
212
|
+
_Note_: By default, we use `arguments.map(&:object_id)` to identify arguments but in some cases,
|
213
|
+
you may want to override it, for example:
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
class User
|
217
|
+
include N1Loader::Loadable
|
218
|
+
|
219
|
+
n1_optimized :orders_count do
|
220
|
+
argument :sale
|
221
|
+
|
222
|
+
cache_key { sale.id }
|
223
|
+
|
224
|
+
def perform(users)
|
225
|
+
orders_per_user = Order.where(sale: sale, user: users).group(:user_id).count
|
226
|
+
|
227
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
user = User.new
|
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
|
235
|
+
```
|
236
|
+
|
237
|
+
|
194
238
|
## Integrations
|
195
239
|
|
196
240
|
### [ActiveRecord][5]
|
197
241
|
|
198
|
-
|
199
|
-
|
200
|
-
_Note_: Please open an issue if you interested in support of version 7 or other.
|
242
|
+
_Note_: Rails 7 support is coming soon! Stay tuned!
|
201
243
|
|
202
244
|
```ruby
|
203
245
|
class User < ActiveRecord::Base
|
204
246
|
include N1Loader::Loadable
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
hash.transform_keys! { |key| users.find { |user| user.id == key } }
|
247
|
+
|
248
|
+
n1_optimized :orders_count do |users|
|
249
|
+
orders_per_user = Order.where(user: users).group(:user_id).count
|
250
|
+
|
251
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
211
252
|
end
|
212
253
|
end
|
213
254
|
|
@@ -231,12 +272,11 @@ users.map(&:orders_count)
|
|
231
272
|
```ruby
|
232
273
|
class User < ActiveRecord::Base
|
233
274
|
include N1Loader::Loadable
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
hash.transform_keys! { |key| users.find { |user| user.id == key } }
|
275
|
+
|
276
|
+
n1_optimized :orders_count do |users|
|
277
|
+
orders_per_user = Order.where(user: users).group(:user_id).count
|
278
|
+
|
279
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
240
280
|
end
|
241
281
|
end
|
242
282
|
|
@@ -253,12 +293,6 @@ ArLazyPreload.config.auto_preload = true
|
|
253
293
|
User.all.map(:orders_count)
|
254
294
|
```
|
255
295
|
|
256
|
-
## Development
|
257
|
-
|
258
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
259
|
-
|
260
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
261
|
-
|
262
296
|
## Contributing
|
263
297
|
|
264
298
|
Bug reports and pull requests are welcome on GitHub at https://github.com/djezzzl/n1_loader.
|
@@ -1,10 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Load core library
|
3
4
|
require_relative "../n1_loader"
|
4
5
|
|
6
|
+
# Load integration dependency
|
5
7
|
require "active_record"
|
6
8
|
|
9
|
+
module N1Loader
|
10
|
+
module ActiveRecord
|
11
|
+
class InvalidPreloading < N1Loader::Error; end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Library integration
|
7
16
|
ActiveSupport.on_load(:active_record) do
|
17
|
+
require_relative "active_record/loader"
|
18
|
+
require_relative "active_record/loader_collection"
|
19
|
+
|
8
20
|
case ActiveRecord::VERSION::MAJOR
|
9
21
|
when 6
|
10
22
|
require_relative "active_record/associations_preloader_v6"
|
@@ -4,9 +4,16 @@ module N1Loader
|
|
4
4
|
module ArLazyPreload
|
5
5
|
# Context builder for N1Loader
|
6
6
|
class AssociatedContextBuilder < ::ArLazyPreload::AssociatedContextBuilder
|
7
|
+
attr_reader :records
|
8
|
+
|
9
|
+
def initialize(parent_context:, association_name:, records:)
|
10
|
+
super(parent_context: parent_context, association_name: association_name)
|
11
|
+
@records = records
|
12
|
+
end
|
13
|
+
|
7
14
|
def perform
|
8
15
|
::ArLazyPreload::Context.register(
|
9
|
-
records:
|
16
|
+
records: records.flatten(1).select { |record| record.respond_to?(:lazy_preload_context=) },
|
10
17
|
association_tree: child_association_tree,
|
11
18
|
auto_preload: parent_context.auto_preload?
|
12
19
|
)
|
@@ -19,12 +19,15 @@ module N1Loader
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def perform_preloading(association_name)
|
22
|
-
|
22
|
+
context_setup = lambda { |records|
|
23
|
+
AssociatedContextBuilder.prepare(
|
24
|
+
parent_context: self,
|
25
|
+
association_name: association_name,
|
26
|
+
records: records
|
27
|
+
)
|
28
|
+
}
|
23
29
|
|
24
|
-
|
25
|
-
parent_context: self,
|
26
|
-
association_name: association_name
|
27
|
-
)
|
30
|
+
N1Loader::Preloader.new(records, context_setup).preload(association_name)
|
28
31
|
end
|
29
32
|
end
|
30
33
|
end
|
@@ -4,18 +4,18 @@ module N1Loader
|
|
4
4
|
module ArLazyPreload
|
5
5
|
module Loadable
|
6
6
|
module ClassMethods # :nodoc:
|
7
|
-
def
|
7
|
+
def n1_optimized(name, loader = nil, &block)
|
8
8
|
name, loader_name, loader_variable_name = super
|
9
9
|
|
10
10
|
define_method(loader_name) do
|
11
11
|
loader = instance_variable_get(loader_variable_name)
|
12
|
-
|
13
12
|
return loader if loader
|
13
|
+
|
14
14
|
if respond_to?(:lazy_preload_context) && ContextAdapter.new(lazy_preload_context).try_preload_lazily(name)
|
15
15
|
return instance_variable_get(loader_variable_name)
|
16
16
|
end
|
17
17
|
|
18
|
-
|
18
|
+
send("#{loader_name}_reload")
|
19
19
|
end
|
20
20
|
end
|
21
21
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module N1Loader
|
4
|
+
module ArLazyPreload
|
5
|
+
# A patch to {N1Loader::LoaderCollection} to setup lazy context lazily.
|
6
|
+
module LoaderCollectionPatch
|
7
|
+
attr_accessor :context_setup
|
8
|
+
|
9
|
+
def with(**args)
|
10
|
+
result = super
|
11
|
+
|
12
|
+
result.context_setup = context_setup if context_setup && result.context_setup.nil?
|
13
|
+
|
14
|
+
result
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module N1Loader
|
4
|
+
module ArLazyPreload
|
5
|
+
# A patch to {N1Loader::Loader} to setup lazy context lazily.
|
6
|
+
module LoaderPatch
|
7
|
+
attr_accessor :context_setup
|
8
|
+
|
9
|
+
def loaded
|
10
|
+
return @loaded if @loaded
|
11
|
+
|
12
|
+
super
|
13
|
+
|
14
|
+
context_setup&.call(preloaded_records)
|
15
|
+
|
16
|
+
@loaded
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module N1Loader
|
4
|
+
module ArLazyPreload
|
5
|
+
# A patch to {N1Loader::Preloader} setup lazy context lazily.
|
6
|
+
module PreloaderPatch
|
7
|
+
def initialize(elements, context_setup = nil)
|
8
|
+
super(elements)
|
9
|
+
@context_setup = context_setup
|
10
|
+
end
|
11
|
+
|
12
|
+
def preload(*keys)
|
13
|
+
super.each do |loader_collection|
|
14
|
+
loader_collection.context_setup = context_setup
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :context_setup
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,12 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Load core library
|
3
4
|
require_relative "active_record"
|
4
5
|
|
6
|
+
# Load integration dependency
|
5
7
|
require "rails"
|
6
8
|
require "ar_lazy_preload"
|
7
9
|
|
10
|
+
# Library integration
|
8
11
|
require_relative "ar_lazy_preload/loadable"
|
9
12
|
require_relative "ar_lazy_preload/context_adapter"
|
10
13
|
require_relative "ar_lazy_preload/associated_context_builder"
|
14
|
+
require_relative "ar_lazy_preload/loader_collection_patch"
|
15
|
+
require_relative "ar_lazy_preload/preloader_patch"
|
16
|
+
require_relative "ar_lazy_preload/loader_patch"
|
11
17
|
|
12
18
|
N1Loader::Loadable::ClassMethods.prepend(N1Loader::ArLazyPreload::Loadable::ClassMethods)
|
19
|
+
N1Loader::Preloader.prepend(N1Loader::ArLazyPreload::PreloaderPatch)
|
20
|
+
N1Loader::Loader.prepend(N1Loader::ArLazyPreload::LoaderPatch)
|
21
|
+
N1Loader::LoaderCollection.prepend(N1Loader::ArLazyPreload::LoaderCollectionPatch)
|
@@ -7,9 +7,9 @@ module N1Loader
|
|
7
7
|
# include N1Loader::Loadable
|
8
8
|
#
|
9
9
|
# # with inline loader
|
10
|
-
# n1_loader :something do
|
11
|
-
# elements
|
12
|
-
#
|
10
|
+
# n1_loader :something do
|
11
|
+
# def perform(elements)
|
12
|
+
# elements.each { |element| fulfill(element,, element.calculate_something) }
|
13
13
|
# end
|
14
14
|
# end
|
15
15
|
#
|
@@ -20,9 +20,7 @@ module N1Loader
|
|
20
20
|
# # custom loader
|
21
21
|
# class MyLoader < N1Loader::Loader
|
22
22
|
# def perform(elements)
|
23
|
-
# elements.
|
24
|
-
# hash[element] = element.calculate_something
|
25
|
-
# end
|
23
|
+
# elements.each { |element| fulfill(element,, element.calculate_something) }
|
26
24
|
# end
|
27
25
|
# end
|
28
26
|
module Loadable
|
@@ -30,8 +28,8 @@ module N1Loader
|
|
30
28
|
send("#{name}_loader")
|
31
29
|
end
|
32
30
|
|
33
|
-
def n1_loader_set(name,
|
34
|
-
send("#{name}_loader=",
|
31
|
+
def n1_loader_set(name, loader_collection)
|
32
|
+
send("#{name}_loader=", loader_collection)
|
35
33
|
end
|
36
34
|
|
37
35
|
def self.included(base)
|
@@ -47,15 +45,14 @@ module N1Loader
|
|
47
45
|
respond_to?("#{name}_loader")
|
48
46
|
end
|
49
47
|
|
50
|
-
def
|
48
|
+
def n1_optimized(name, loader = nil, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
51
49
|
loader ||= Class.new(N1Loader::Loader) do
|
52
|
-
if block
|
50
|
+
if block.arity == 1
|
53
51
|
define_method(:perform, &block)
|
54
52
|
else
|
55
53
|
class_eval(&block)
|
56
54
|
end
|
57
55
|
end
|
58
|
-
|
59
56
|
loader_name = "#{name}_loader"
|
60
57
|
loader_variable_name = "@#{loader_name}"
|
61
58
|
|
@@ -64,21 +61,22 @@ module N1Loader
|
|
64
61
|
end
|
65
62
|
|
66
63
|
define_method("#{loader_name}_reload") do
|
67
|
-
instance_variable_set(loader_variable_name,
|
64
|
+
instance_variable_set(loader_variable_name,
|
65
|
+
N1Loader::LoaderCollection.new(self.class.send(loader_name), [self]))
|
68
66
|
end
|
69
67
|
|
70
|
-
define_method("#{loader_name}=") do |
|
71
|
-
instance_variable_set(loader_variable_name,
|
68
|
+
define_method("#{loader_name}=") do |loader_collection_instance|
|
69
|
+
instance_variable_set(loader_variable_name, loader_collection_instance)
|
72
70
|
end
|
73
71
|
|
74
72
|
define_method(loader_name) do
|
75
73
|
instance_variable_get(loader_variable_name) || send("#{loader_name}_reload")
|
76
74
|
end
|
77
75
|
|
78
|
-
define_method(name) do |reload: false|
|
76
|
+
define_method(name) do |reload: false, **args|
|
79
77
|
send("#{loader_name}_reload") if reload
|
80
78
|
|
81
|
-
send(loader_name).for(self)
|
79
|
+
send(loader_name).with(**args).for(self)
|
82
80
|
end
|
83
81
|
|
84
82
|
[name, loader_name, loader_variable_name]
|
@@ -6,34 +6,112 @@ 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
|
-
|
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
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(elements, **args)
|
10
38
|
@elements = elements
|
39
|
+
@args = args
|
11
40
|
end
|
12
41
|
|
13
|
-
def
|
14
|
-
|
42
|
+
def for(element)
|
43
|
+
if loaded.empty? && elements.any?
|
44
|
+
raise NotFilled, "Nothing was preloaded, perhaps you forgot to use fulfill method"
|
45
|
+
end
|
46
|
+
raise NotLoaded, "The data was not preloaded for the given element" unless loaded.key?(element)
|
47
|
+
|
48
|
+
loaded[element]
|
15
49
|
end
|
16
50
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
51
|
+
def cache_key
|
52
|
+
check_arguments!
|
53
|
+
args.values.map(&:object_id)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
attr_reader :elements, :args
|
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"
|
23
76
|
end
|
24
77
|
|
25
|
-
def
|
26
|
-
|
78
|
+
def check_arguments!
|
79
|
+
check_missing_arguments!
|
80
|
+
check_invalid_arguments!
|
27
81
|
end
|
28
82
|
|
29
|
-
def
|
30
|
-
|
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 }
|
31
88
|
|
32
|
-
|
89
|
+
raise InvalidArgument, "Loader doesn't define #{arg} argument"
|
90
|
+
end
|
33
91
|
end
|
34
92
|
|
35
|
-
|
93
|
+
def perform(_elements)
|
94
|
+
raise NotImplemented, "Subclasses have to implement the method"
|
95
|
+
end
|
36
96
|
|
37
|
-
|
97
|
+
def fulfill(element, value)
|
98
|
+
@loaded[element] = value
|
99
|
+
end
|
100
|
+
|
101
|
+
def loaded
|
102
|
+
return @loaded if @loaded
|
103
|
+
|
104
|
+
check_arguments!
|
105
|
+
|
106
|
+
@loaded = {}.compare_by_identity
|
107
|
+
|
108
|
+
if elements.size == 1 && respond_to?(:single)
|
109
|
+
fulfill(elements.first, single(elements.first))
|
110
|
+
elsif elements.any?
|
111
|
+
perform(elements)
|
112
|
+
end
|
113
|
+
|
114
|
+
@loaded
|
115
|
+
end
|
38
116
|
end
|
39
117
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module N1Loader
|
4
|
+
# The class is used for storing collections of loaders for elements per set of arguments.
|
5
|
+
class LoaderCollection
|
6
|
+
attr_reader :loader_class, :elements
|
7
|
+
|
8
|
+
def initialize(loader_class, elements)
|
9
|
+
@loader_class = loader_class
|
10
|
+
@elements = elements
|
11
|
+
end
|
12
|
+
|
13
|
+
def with(**args)
|
14
|
+
loader = loader_class.new(elements, **args)
|
15
|
+
|
16
|
+
loaders[loader.cache_key] ||= loader
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def loaders
|
22
|
+
@loaders ||= {}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -19,9 +19,9 @@ module N1Loader
|
|
19
19
|
elements
|
20
20
|
.group_by { |element| element.class.n1_loader(key) }
|
21
21
|
.map do |loader_class, grouped_elements|
|
22
|
-
|
23
|
-
grouped_elements.each { |grouped_element| grouped_element.n1_loader_set(key,
|
24
|
-
|
22
|
+
loader_collection = N1Loader::LoaderCollection.new(loader_class, grouped_elements)
|
23
|
+
grouped_elements.each { |grouped_element| grouped_element.n1_loader_set(key, loader_collection) }
|
24
|
+
loader_collection
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
data/lib/n1_loader/version.rb
CHANGED
data/lib/n1_loader.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require_relative "n1_loader/version"
|
4
4
|
|
5
5
|
require_relative "n1_loader/core/loader"
|
6
|
+
require_relative "n1_loader/core/loader_collection"
|
6
7
|
require_relative "n1_loader/core/loadable"
|
7
8
|
require_relative "n1_loader/core/preloader"
|
8
9
|
|
@@ -10,4 +11,7 @@ module N1Loader # :nodoc:
|
|
10
11
|
class Error < StandardError; end
|
11
12
|
class NotImplemented < Error; end
|
12
13
|
class NotLoaded < Error; end
|
14
|
+
class NotFilled < Error; end
|
15
|
+
class MissingArgument < Error; end
|
16
|
+
class InvalidArgument < Error; end
|
13
17
|
end
|
data/n1_loader.gemspec
CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.authors = ["Evgeniy Demin"]
|
9
9
|
spec.email = ["lawliet.djez@gmail.com"]
|
10
10
|
|
11
|
-
spec.summary = "
|
11
|
+
spec.summary = "Loader to solve N+1 issue for good."
|
12
12
|
spec.homepage = "https://github.com/djezzzl/n1_loader"
|
13
13
|
spec.license = "MIT"
|
14
14
|
spec.required_ruby_version = ">= 2.5.0"
|
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.require_paths = ["lib"]
|
24
24
|
|
25
25
|
spec.add_development_dependency "activerecord", ">= 5"
|
26
|
-
spec.add_development_dependency "ar_lazy_preload", "
|
26
|
+
spec.add_development_dependency "ar_lazy_preload", ">= 0.6"
|
27
27
|
spec.add_development_dependency "db-query-matchers", "~> 0.10"
|
28
28
|
spec.add_development_dependency "rails", ">= 5"
|
29
29
|
spec.add_development_dependency "rspec", "~> 3.0"
|
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.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Evgeniy Demin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-02-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -28,16 +28,16 @@ dependencies:
|
|
28
28
|
name: ar_lazy_preload
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '0.
|
33
|
+
version: '0.6'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '0.
|
40
|
+
version: '0.6'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: db-query-matchers
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -139,20 +139,28 @@ files:
|
|
139
139
|
- LICENSE.txt
|
140
140
|
- README.md
|
141
141
|
- Rakefile
|
142
|
+
- activerecord-gemfiles/ar_5_latest.gemfile
|
143
|
+
- activerecord-gemfiles/ar_6_latest.gemfile
|
144
|
+
- ar_lazy_preload-gemfiles/ar_lazy_preload_0.6.1.gemfile
|
145
|
+
- ar_lazy_preload-gemfiles/ar_lazy_preload_master.gemfile
|
142
146
|
- bin/console
|
143
147
|
- bin/setup
|
144
|
-
- gemfiles/ar_5_latest.gemfile
|
145
|
-
- gemfiles/ar_6_latest.gemfile
|
146
148
|
- lib/n1_loader.rb
|
147
149
|
- lib/n1_loader/active_record.rb
|
148
150
|
- lib/n1_loader/active_record/associations_preloader_v5.rb
|
149
151
|
- lib/n1_loader/active_record/associations_preloader_v6.rb
|
152
|
+
- lib/n1_loader/active_record/loader.rb
|
153
|
+
- lib/n1_loader/active_record/loader_collection.rb
|
150
154
|
- lib/n1_loader/ar_lazy_preload.rb
|
151
155
|
- lib/n1_loader/ar_lazy_preload/associated_context_builder.rb
|
152
156
|
- lib/n1_loader/ar_lazy_preload/context_adapter.rb
|
153
157
|
- lib/n1_loader/ar_lazy_preload/loadable.rb
|
158
|
+
- lib/n1_loader/ar_lazy_preload/loader_collection_patch.rb
|
159
|
+
- lib/n1_loader/ar_lazy_preload/loader_patch.rb
|
160
|
+
- lib/n1_loader/ar_lazy_preload/preloader_patch.rb
|
154
161
|
- lib/n1_loader/core/loadable.rb
|
155
162
|
- lib/n1_loader/core/loader.rb
|
163
|
+
- lib/n1_loader/core/loader_collection.rb
|
156
164
|
- lib/n1_loader/core/preloader.rb
|
157
165
|
- lib/n1_loader/version.rb
|
158
166
|
- n1_loader.gemspec
|
@@ -181,5 +189,5 @@ requirements: []
|
|
181
189
|
rubygems_version: 3.2.22
|
182
190
|
signing_key:
|
183
191
|
specification_version: 4
|
184
|
-
summary:
|
192
|
+
summary: Loader to solve N+1 issue for good.
|
185
193
|
test_files: []
|