n1_loader 1.1.0 → 1.2.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 +4 -0
- data/Gemfile +9 -0
- data/README.md +116 -79
- 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_collection.rb +9 -0
- data/lib/n1_loader/active_record.rb +13 -1
- 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 +2 -2
- 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 +13 -11
- data/lib/n1_loader/core/loader.rb +9 -4
- data/lib/n1_loader/core/loader_collection.rb +23 -0
- data/lib/n1_loader/core/preloader.rb +3 -3
- data/lib/n1_loader/version.rb +1 -1
- data/lib/n1_loader.rb +1 -0
- data/n1_loader.gemspec +2 -2
- metadata +16 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8b9b61121f94d9dba30e969c1b9a3fa9c812292d618434a98262c90aef43e6cd
|
4
|
+
data.tar.gz: a1ecf5359a1a53200e354b30289566a437cf372c54ccf3f13c2e0adc2cb71640
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5c545cc2033dc8e75edddd5c948eed78fc1d965464522147841d30e3c6a02aebb0b6758d3f801b020c25d62618f7ae4bbb7ff11441748f798546351b1bf34d85
|
7
|
+
data.tar.gz: a60fa813399debf1ae9c8f839272a683c2a7f733ea392b4b1bd4169af55369a1d3be2fd8648a548e50093005e3d2df962437adf08f1f9fdf1338fa4ab6217df7
|
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
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,119 +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
|
-
n1_loader :
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
# with custom loader
|
54
|
-
n1_loader :something, MyLoader
|
55
|
-
end
|
56
|
-
|
57
|
-
# Custom loader that can be shared with many classes
|
58
|
-
class MyLoader < N1Loader::Loader
|
59
|
-
def perform(elements)
|
60
|
-
elements.each { |element| fulfill(element, [element]) }
|
48
|
+
n1_loader :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]) }
|
61
52
|
end
|
62
53
|
end
|
63
54
|
|
64
55
|
# For single object
|
65
|
-
|
66
|
-
|
56
|
+
user = User.new
|
57
|
+
user.orders_count
|
67
58
|
|
68
59
|
# For multiple objects without N+1
|
69
|
-
|
70
|
-
N1Loader::Preloader.new(
|
71
|
-
|
60
|
+
users = [User.new, User.new]
|
61
|
+
N1Loader::Preloader.new(users).preload(:orders_count)
|
62
|
+
users.map(&:orders_count)
|
72
63
|
```
|
73
64
|
|
74
65
|
### Lazy loading
|
75
66
|
|
76
67
|
```ruby
|
77
|
-
class
|
68
|
+
class User
|
78
69
|
include N1Loader::Loadable
|
79
|
-
|
80
|
-
|
81
|
-
|
70
|
+
|
71
|
+
# with inline loader
|
72
|
+
n1_loader :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]) }
|
82
76
|
end
|
83
77
|
end
|
84
78
|
|
85
|
-
|
86
|
-
|
79
|
+
user = User.new # => nothing was done for loading
|
80
|
+
user.orders_count # => first time loading
|
87
81
|
|
88
|
-
|
89
|
-
N1Loader::Preloader.new([
|
90
|
-
|
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)
|
91
85
|
```
|
92
86
|
|
93
87
|
|
94
88
|
### Shareable loaders
|
95
89
|
|
96
90
|
```ruby
|
97
|
-
class
|
98
|
-
def perform(
|
99
|
-
|
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]) }
|
100
96
|
end
|
101
97
|
end
|
102
98
|
|
103
|
-
class
|
99
|
+
class User
|
104
100
|
include N1Loader::Loadable
|
105
101
|
|
106
|
-
n1_loader :
|
102
|
+
n1_loader :orders_count, OrdersCountLoader
|
107
103
|
end
|
108
104
|
|
109
|
-
class
|
105
|
+
class Customer
|
110
106
|
include N1Loader::Loadable
|
111
107
|
|
112
|
-
n1_loader :
|
108
|
+
n1_loader :orders_count, OrdersCountLoader
|
113
109
|
end
|
114
110
|
|
115
|
-
|
116
|
-
|
111
|
+
User.new.orders_count # => works
|
112
|
+
Customer.new.orders_count # => works
|
117
113
|
```
|
118
114
|
|
119
115
|
### Reloading
|
120
116
|
|
121
117
|
```ruby
|
122
|
-
class
|
118
|
+
class User
|
123
119
|
include N1Loader::Loadable
|
124
120
|
|
125
121
|
# with inline loader
|
126
|
-
n1_loader :
|
127
|
-
|
122
|
+
n1_loader :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]) }
|
128
126
|
end
|
129
127
|
end
|
130
128
|
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
134
132
|
|
135
|
-
|
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
|
136
136
|
|
137
|
-
N1Loader::Preloader.new(
|
138
|
-
|
139
|
-
|
140
|
-
N1Loader::Preloader.new(objects).preload(:anything) # => loader was initialized again but not yet executed
|
141
|
-
objects.map(&:anything) # => new loader was executed first time without N+1 issue and new values were cached
|
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
|
142
139
|
```
|
143
140
|
|
144
141
|
### Isolated loaders
|
145
142
|
|
146
143
|
```ruby
|
147
|
-
class
|
144
|
+
class IsolatedLoader < N1Loader::Loader
|
148
145
|
def perform(elements)
|
149
146
|
elements.each { |element| fulfill(element, [element]) }
|
150
147
|
end
|
151
148
|
end
|
152
149
|
|
153
150
|
objects = [1, 2, 3, 4]
|
154
|
-
loader =
|
151
|
+
loader = IsolatedLoader.new(objects)
|
155
152
|
objects.each do |object|
|
156
153
|
loader.for(object) # => it has no N+1 and it doesn't require to be injected in the class
|
157
154
|
end
|
@@ -160,46 +157,92 @@ end
|
|
160
157
|
### Optimized single case
|
161
158
|
|
162
159
|
```ruby
|
163
|
-
class
|
160
|
+
class User
|
164
161
|
include N1Loader::Loadable
|
165
162
|
|
166
|
-
n1_loader :
|
167
|
-
def perform(
|
168
|
-
|
163
|
+
n1_loader :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]) }
|
169
168
|
end
|
170
169
|
|
171
170
|
# Optimized for single object loading
|
172
|
-
def single(
|
173
|
-
|
174
|
-
[element]
|
171
|
+
def single(user)
|
172
|
+
user.orders.count
|
175
173
|
end
|
176
174
|
end
|
177
175
|
end
|
178
176
|
|
179
|
-
|
180
|
-
|
177
|
+
user = User.new
|
178
|
+
user.orders_count # single will be used here
|
181
179
|
|
182
|
-
|
183
|
-
N1Loader::Preloader.new(
|
184
|
-
|
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
|
185
183
|
```
|
186
184
|
|
187
|
-
|
185
|
+
### Arguments
|
188
186
|
|
189
|
-
|
187
|
+
```ruby
|
188
|
+
class User
|
189
|
+
include N1Loader::Loadable
|
190
|
+
|
191
|
+
n1_loader :orders_count do |users, type|
|
192
|
+
orders_per_user = Order.where(type: type, user: users).group(:user_id).count
|
193
|
+
|
194
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
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
|
202
|
+
|
203
|
+
users = [User.new, User.new]
|
204
|
+
N1Loader::Preloader.new(users).preload(:orders_count)
|
205
|
+
users.map { |user| user.orders_count(:gifts) } # No N+1 here
|
206
|
+
```
|
207
|
+
|
208
|
+
_Note_: By default, we use `arguments.map(&:object_id)` to identify arguments but in some cases,
|
209
|
+
you may want to override it, for example:
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
class User
|
213
|
+
include N1Loader::Loadable
|
214
|
+
|
215
|
+
n1_loader :orders_count do
|
216
|
+
def perform(users, sale)
|
217
|
+
orders_per_user = Order.where(sale: sale, user: users).group(:user_id).count
|
218
|
+
|
219
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
220
|
+
end
|
221
|
+
|
222
|
+
def self.arguments_key(sale)
|
223
|
+
sale.id
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
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
|
231
|
+
```
|
190
232
|
|
191
|
-
**Supported versions**: 5, 6.
|
192
233
|
|
193
|
-
|
234
|
+
## Integrations
|
235
|
+
|
236
|
+
### [ActiveRecord][5]
|
194
237
|
|
195
238
|
```ruby
|
196
239
|
class User < ActiveRecord::Base
|
197
240
|
include N1Loader::Loadable
|
198
241
|
|
199
242
|
n1_loader :orders_count do |users|
|
200
|
-
|
201
|
-
|
202
|
-
users.each { |user| fulfill(user,
|
243
|
+
orders_per_user = Order.where(user: users).group(:user_id).count
|
244
|
+
|
245
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
203
246
|
end
|
204
247
|
end
|
205
248
|
|
@@ -225,9 +268,9 @@ class User < ActiveRecord::Base
|
|
225
268
|
include N1Loader::Loadable
|
226
269
|
|
227
270
|
n1_loader :orders_count do |users|
|
228
|
-
|
271
|
+
orders_per_user = Order.where(user: users).group(:user_id).count
|
229
272
|
|
230
|
-
users.each { |user| fulfill(user,
|
273
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
231
274
|
end
|
232
275
|
end
|
233
276
|
|
@@ -244,12 +287,6 @@ ArLazyPreload.config.auto_preload = true
|
|
244
287
|
User.all.map(:orders_count)
|
245
288
|
```
|
246
289
|
|
247
|
-
## Development
|
248
|
-
|
249
|
-
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.
|
250
|
-
|
251
|
-
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).
|
252
|
-
|
253
290
|
## Contributing
|
254
291
|
|
255
292
|
Bug reports and pull requests are welcome on GitHub at https://github.com/djezzzl/n1_loader.
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
N1Loader::LoaderCollection.define_method :preloaded_records do
|
4
|
+
unless loader_class.instance_method(:perform).arity == 1
|
5
|
+
raise N1Loader::ActiveRecord::InvalidPreloading, "Cannot preload loader with arguments"
|
6
|
+
end
|
7
|
+
|
8
|
+
with.preloaded_records
|
9
|
+
end
|
@@ -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
|
@@ -9,13 +9,13 @@ module N1Loader
|
|
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,8 +7,10 @@ module N1Loader
|
|
7
7
|
# include N1Loader::Loadable
|
8
8
|
#
|
9
9
|
# # with inline loader
|
10
|
-
# n1_loader :something do
|
11
|
-
#
|
10
|
+
# n1_loader :something do
|
11
|
+
# def perform(elements)
|
12
|
+
# elements.each { |element| fulfill(element,, element.calculate_something) }
|
13
|
+
# end
|
12
14
|
# end
|
13
15
|
#
|
14
16
|
# # with custom loader
|
@@ -26,8 +28,8 @@ module N1Loader
|
|
26
28
|
send("#{name}_loader")
|
27
29
|
end
|
28
30
|
|
29
|
-
def n1_loader_set(name,
|
30
|
-
send("#{name}_loader=",
|
31
|
+
def n1_loader_set(name, loader_collection)
|
32
|
+
send("#{name}_loader=", loader_collection)
|
31
33
|
end
|
32
34
|
|
33
35
|
def self.included(base)
|
@@ -45,13 +47,12 @@ module N1Loader
|
|
45
47
|
|
46
48
|
def n1_load(name, loader = nil, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
47
49
|
loader ||= Class.new(N1Loader::Loader) do
|
48
|
-
if block
|
50
|
+
if block&.arity&.positive?
|
49
51
|
define_method(:perform, &block)
|
50
52
|
else
|
51
53
|
class_eval(&block)
|
52
54
|
end
|
53
55
|
end
|
54
|
-
|
55
56
|
loader_name = "#{name}_loader"
|
56
57
|
loader_variable_name = "@#{loader_name}"
|
57
58
|
|
@@ -60,21 +61,22 @@ module N1Loader
|
|
60
61
|
end
|
61
62
|
|
62
63
|
define_method("#{loader_name}_reload") do
|
63
|
-
instance_variable_set(loader_variable_name,
|
64
|
+
instance_variable_set(loader_variable_name,
|
65
|
+
N1Loader::LoaderCollection.new(self.class.send(loader_name), [self]))
|
64
66
|
end
|
65
67
|
|
66
|
-
define_method("#{loader_name}=") do |
|
67
|
-
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)
|
68
70
|
end
|
69
71
|
|
70
72
|
define_method(loader_name) do
|
71
73
|
instance_variable_get(loader_variable_name) || send("#{loader_name}_reload")
|
72
74
|
end
|
73
75
|
|
74
|
-
define_method(name) do
|
76
|
+
define_method(name) do |*args, reload: false|
|
75
77
|
send("#{loader_name}_reload") if reload
|
76
78
|
|
77
|
-
send(loader_name).for(self)
|
79
|
+
send(loader_name).with(*args).for(self)
|
78
80
|
end
|
79
81
|
|
80
82
|
[name, loader_name, loader_variable_name]
|
@@ -6,8 +6,13 @@ 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
|
-
def
|
9
|
+
def self.arguments_key(*args)
|
10
|
+
args.map(&:object_id)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(elements, *args)
|
10
14
|
@elements = elements
|
15
|
+
@args = args
|
11
16
|
end
|
12
17
|
|
13
18
|
def for(element)
|
@@ -21,7 +26,7 @@ module N1Loader
|
|
21
26
|
|
22
27
|
private
|
23
28
|
|
24
|
-
attr_reader :elements
|
29
|
+
attr_reader :elements, :args
|
25
30
|
|
26
31
|
def perform(_elements)
|
27
32
|
raise NotImplemented, "Subclasses have to implement the method"
|
@@ -37,9 +42,9 @@ module N1Loader
|
|
37
42
|
@loaded = {}.compare_by_identity
|
38
43
|
|
39
44
|
if elements.size == 1 && respond_to?(:single)
|
40
|
-
fulfill(elements.first, single(elements.first))
|
45
|
+
fulfill(elements.first, single(elements.first, *args))
|
41
46
|
elsif elements.any?
|
42
|
-
perform(elements)
|
47
|
+
perform(elements, *args)
|
43
48
|
end
|
44
49
|
|
45
50
|
@loaded
|
@@ -0,0 +1,23 @@
|
|
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
|
+
loaders[loader_class.arguments_key(*args)] ||= loader_class.new(elements, *args)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def loaders
|
20
|
+
@loaders ||= {}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
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
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.2.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-01-14 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,21 +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
|
150
152
|
- lib/n1_loader/active_record/loader.rb
|
153
|
+
- lib/n1_loader/active_record/loader_collection.rb
|
151
154
|
- lib/n1_loader/ar_lazy_preload.rb
|
152
155
|
- lib/n1_loader/ar_lazy_preload/associated_context_builder.rb
|
153
156
|
- lib/n1_loader/ar_lazy_preload/context_adapter.rb
|
154
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
|
155
161
|
- lib/n1_loader/core/loadable.rb
|
156
162
|
- lib/n1_loader/core/loader.rb
|
163
|
+
- lib/n1_loader/core/loader_collection.rb
|
157
164
|
- lib/n1_loader/core/preloader.rb
|
158
165
|
- lib/n1_loader/version.rb
|
159
166
|
- n1_loader.gemspec
|
@@ -182,5 +189,5 @@ requirements: []
|
|
182
189
|
rubygems_version: 3.2.22
|
183
190
|
signing_key:
|
184
191
|
specification_version: 4
|
185
|
-
summary:
|
192
|
+
summary: Loader to solve N+1 issue for good.
|
186
193
|
test_files: []
|