batch-loader 0.2.0 → 0.3.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/.travis.yml +2 -0
- data/CHANGELOG.md +9 -4
- data/Gemfile +1 -1
- data/README.md +121 -18
- data/batch-loader.gemspec +2 -0
- data/lib/batch_loader.rb +13 -15
- data/lib/batch_loader/executor.rb +5 -1
- data/lib/batch_loader/middleware.rb +1 -2
- data/lib/batch_loader/version.rb +1 -1
- metadata +30 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e17f7617e248aa7b911c64dac882cdb192bb9b2c
|
4
|
+
data.tar.gz: 6b9d612ae58f95a0715c0d23427176375d8450c4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b34fa00da8e309c99b7bae84e8155dcd847722f5da30d60c19a318a114459d825645ef3377259219e5fa9c64c1ffd1495e4ac78bf4907ab811f66b8a8b0ba178
|
7
|
+
data.tar.gz: 7e97b3aa40becb1515a3a26d2a1992f2ccbfda2a4f5f090fc7c329e898319aa765edec379f8a0070d555276a0ab00cb24d9cf4235b230d0e7fcb6a0e0bce6625
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -8,15 +8,20 @@ one of the following labels: `Added`, `Changed`, `Deprecated`,
|
|
8
8
|
to manage the versions of this gem so
|
9
9
|
that you can set version constraints properly.
|
10
10
|
|
11
|
-
#### [Unreleased](https://github.com/exAspArk/batch-loader/compare/v0.
|
11
|
+
#### [Unreleased](https://github.com/exAspArk/batch-loader/compare/v0.3.0...HEAD)
|
12
12
|
|
13
13
|
* WIP
|
14
14
|
|
15
|
+
#### [v0.3.0](https://github.com/exAspArk/batch-loader/compare/v0.2.0...v0.3.0) – 2017-08-03
|
16
|
+
|
17
|
+
* `Added`: `BatchLoader::Executor.clear_current` to clear cache manually.
|
18
|
+
* `Added`: tests and description how to use with GraphQL.
|
19
|
+
|
15
20
|
#### [v0.2.0](https://github.com/exAspArk/batch-loader/compare/v0.1.0...v0.2.0) – 2017-08-02
|
16
21
|
|
17
|
-
* `Added`: `cache: false` option.
|
18
|
-
* `Added`: `BatchLoader::Middleware
|
19
|
-
* `Added`:
|
22
|
+
* `Added`: `cache: false` option to disable caching for resolved values.
|
23
|
+
* `Added`: `BatchLoader::Middleware` to clear cache between Rack requests.
|
24
|
+
* `Added`: more docs and tests.
|
20
25
|
|
21
26
|
#### [v0.1.0](https://github.com/exAspArk/batch-loader/compare/ed32edb...v0.1.0) – 2017-07-31
|
22
27
|
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
# BatchLoader
|
2
2
|
|
3
3
|
[](https://travis-ci.org/exAspArk/batch-loader)
|
4
|
+
[](https://coveralls.io/github/exAspArk/batch-loader)
|
5
|
+
[](https://codeclimate.com/github/exAspArk/batch-loader)
|
6
|
+
[](https://rubygems.org/gems/batch-loader)
|
7
|
+
[](https://rubygems.org/gems/batch-loader)
|
4
8
|
|
5
9
|
Simple tool to avoid N+1 DB queries, HTTP requests, etc.
|
6
10
|
|
@@ -16,7 +20,6 @@ Simple tool to avoid N+1 DB queries, HTTP requests, etc.
|
|
16
20
|
* [Caching](#caching)
|
17
21
|
* [Installation](#installation)
|
18
22
|
* [Implementation details](#implementation-details)
|
19
|
-
* [Testing](#testing)
|
20
23
|
* [Development](#development)
|
21
24
|
* [Contributing](#contributing)
|
22
25
|
* [License](#license)
|
@@ -96,7 +99,7 @@ users = load_posts(post.user) # ↓ U ↓
|
|
96
99
|
users.map { |u| user.name } # Users
|
97
100
|
```
|
98
101
|
|
99
|
-
But the problem here is that `load_posts` now depends on the child association
|
102
|
+
But the problem here is that `load_posts` now depends on the child association and knows that it has to preload the data for `load_users`. And it'll do it every time, even if it's not necessary. Can we do better? Sure!
|
100
103
|
|
101
104
|
### Basic example
|
102
105
|
|
@@ -130,7 +133,7 @@ As we can see, batching is isolated and described right in a place where it's ne
|
|
130
133
|
|
131
134
|
### How it works
|
132
135
|
|
133
|
-
In general, `BatchLoader` returns
|
136
|
+
In general, `BatchLoader` returns a lazy object. In other programming languages it usually called Promise, but I personally prefer to call it lazy, since Ruby already uses the name in standard library :) Each lazy object knows which data it needs to load and how to batch the query. When all the lazy objects are collected it's possible to resolve them once without N+1 queries.
|
134
137
|
|
135
138
|
So, when we call `BatchLoader.for` we pass an item (`user_id`) which should be batched. For the `batch` method, we pass a block which uses all the collected items (`user_ids`):
|
136
139
|
|
@@ -172,7 +175,7 @@ end
|
|
172
175
|
class PostsController < ApplicationController
|
173
176
|
def index
|
174
177
|
posts = Post.limit(10)
|
175
|
-
serialized_posts = posts.map { |post| {id: post.id, rating: post.rating} }
|
178
|
+
serialized_posts = posts.map { |post| {id: post.id, rating: post.rating} } # N+1 HTTP requests for each post.rating
|
176
179
|
|
177
180
|
render json: serialized_posts
|
178
181
|
end
|
@@ -182,7 +185,6 @@ end
|
|
182
185
|
As we can see, the code above will make N+1 HTTP requests, one for each post. Let's batch the requests with a gem called [parallel](https://github.com/grosser/parallel):
|
183
186
|
|
184
187
|
```ruby
|
185
|
-
# app/models/post.rb
|
186
188
|
class Post < ApplicationRecord
|
187
189
|
def rating_lazy
|
188
190
|
BatchLoader.for(post).batch do |posts, batch_loader|
|
@@ -190,9 +192,7 @@ class Post < ApplicationRecord
|
|
190
192
|
end
|
191
193
|
end
|
192
194
|
|
193
|
-
|
194
|
-
HttpClient.request(:get, "https://example.com/ratings/#{id}")
|
195
|
-
end
|
195
|
+
# ...
|
196
196
|
end
|
197
197
|
```
|
198
198
|
|
@@ -201,7 +201,6 @@ end
|
|
201
201
|
Now we can resolve all `BatchLoader` objects in the controller:
|
202
202
|
|
203
203
|
```ruby
|
204
|
-
# app/controllers/posts_controller.rb
|
205
204
|
class PostsController < ApplicationController
|
206
205
|
def index
|
207
206
|
posts = Post.limit(10)
|
@@ -211,10 +210,9 @@ class PostsController < ApplicationController
|
|
211
210
|
end
|
212
211
|
```
|
213
212
|
|
214
|
-
`BatchLoader` caches the resolved values. To ensure that the cache is purged
|
213
|
+
`BatchLoader` caches the resolved values. To ensure that the cache is purged between requests in the app add the following middleware to your `config/application.rb`:
|
215
214
|
|
216
215
|
```ruby
|
217
|
-
# config/application.rb
|
218
216
|
config.middleware.use BatchLoader::Middleware
|
219
217
|
```
|
220
218
|
|
@@ -222,11 +220,120 @@ See the [Caching](#caching) section for more information.
|
|
222
220
|
|
223
221
|
### GraphQL example
|
224
222
|
|
225
|
-
|
223
|
+
With GraphQL using batching is particularly useful. You can't use usual techniques such as preloading associations in advance to avoid N+1 queries.
|
224
|
+
Since you don't know which fields user is going to ask in a query.
|
225
|
+
|
226
|
+
Let's take a look at the simple [graphql-ruby](https://github.com/rmosolgo/graphql-ruby) schema example:
|
227
|
+
|
228
|
+
```ruby
|
229
|
+
Schema = GraphQL::Schema.define do
|
230
|
+
query QueryType
|
231
|
+
end
|
232
|
+
|
233
|
+
QueryType = GraphQL::ObjectType.define do
|
234
|
+
name "Query"
|
235
|
+
field :posts, !types[PostType], resolve: ->(obj, args, ctx) { Post.all }
|
236
|
+
end
|
237
|
+
|
238
|
+
PostType = GraphQL::ObjectType.define do
|
239
|
+
name "Post"
|
240
|
+
field :user, !UserType, resolve: ->(post, args, ctx) { post.user } # N+1 queries
|
241
|
+
end
|
242
|
+
|
243
|
+
UserType = GraphQL::ObjectType.define do
|
244
|
+
name "User"
|
245
|
+
field :name, !types.String
|
246
|
+
end
|
247
|
+
```
|
248
|
+
|
249
|
+
If we want to execute a simple query like:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
query = "
|
253
|
+
{
|
254
|
+
posts {
|
255
|
+
user {
|
256
|
+
name
|
257
|
+
}
|
258
|
+
}
|
259
|
+
}
|
260
|
+
"
|
261
|
+
Schema.execute(query, variables: {}, context: {})
|
262
|
+
```
|
263
|
+
|
264
|
+
We will get N+1 queries for each `post.user`. To avoid this problem, all we have to do is to change the resolver to use `BatchLoader`:
|
265
|
+
|
266
|
+
```ruby
|
267
|
+
PostType = GraphQL::ObjectType.define do
|
268
|
+
name "Post"
|
269
|
+
field :user, !UserType, resolve: ->(post, args, ctx) do
|
270
|
+
BatchLoader.for(post.user_id).batch do |user_ids, batch_loader|
|
271
|
+
User.where(id: user_ids).each { |user| batch_loader.load(user.id, user) }
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
```
|
276
|
+
|
277
|
+
And setup GraphQL with built-in `lazy_resolve` method:
|
278
|
+
|
279
|
+
```ruby
|
280
|
+
Schema = GraphQL::Schema.define do
|
281
|
+
query QueryType
|
282
|
+
lazy_resolve BatchLoader, :sync
|
283
|
+
end
|
284
|
+
```
|
226
285
|
|
227
286
|
### Caching
|
228
287
|
|
229
|
-
|
288
|
+
By default `BatchLoader` caches the resolved values. You can test it by running something like:
|
289
|
+
|
290
|
+
```ruby
|
291
|
+
def user_lazy(id)
|
292
|
+
BatchLoader.for(id).batch do |ids, batch_loader|
|
293
|
+
User.where(id: ids).each { |user| batch_loader.load(user.id, user) }
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
user_lazy(1) # no request
|
298
|
+
# => <#BatchLoader>
|
299
|
+
|
300
|
+
user_lazy(1).sync # SELECT * FROM users WHERE id IN (1)
|
301
|
+
# => <#User>
|
302
|
+
|
303
|
+
user_lazy(1).sync # no request
|
304
|
+
# => <#User>
|
305
|
+
```
|
306
|
+
|
307
|
+
To drop the cache manually you can run:
|
308
|
+
|
309
|
+
```ruby
|
310
|
+
user_lazy(1).sync # SELECT * FROM users WHERE id IN (1)
|
311
|
+
user_lazy(1).sync # no request
|
312
|
+
|
313
|
+
BatchLoader::Executor.clear_current
|
314
|
+
|
315
|
+
user_lazy(1).sync # SELECT * FROM users WHERE id IN (1)
|
316
|
+
```
|
317
|
+
|
318
|
+
Usually, it's just enough to clear the cache between HTTP requests in the app. To do so, simply add the middleware:
|
319
|
+
|
320
|
+
```ruby
|
321
|
+
# calls "BatchLoader::Executor.clear_current" after each request
|
322
|
+
use BatchLoader::Middleware
|
323
|
+
```
|
324
|
+
|
325
|
+
In some rare cases it's useful to disable caching for `BatchLoader`. For example, in tests or after data mutations:
|
326
|
+
|
327
|
+
```ruby
|
328
|
+
def user_lazy(id)
|
329
|
+
BatchLoader.for(id).batch(cache: false) do |ids, batch_loader|
|
330
|
+
# ...
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
user_lazy(1).sync # SELECT * FROM users WHERE id IN (1)
|
335
|
+
user_lazy(1).sync # SELECT * FROM users WHERE id IN (1)
|
336
|
+
```
|
230
337
|
|
231
338
|
## Installation
|
232
339
|
|
@@ -246,11 +353,7 @@ Or install it yourself as:
|
|
246
353
|
|
247
354
|
## Implementation details
|
248
355
|
|
249
|
-
|
250
|
-
|
251
|
-
## Testing
|
252
|
-
|
253
|
-
TODO
|
356
|
+
Coming soon
|
254
357
|
|
255
358
|
## Development
|
256
359
|
|
data/batch-loader.gemspec
CHANGED
@@ -26,4 +26,6 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.add_development_dependency "bundler", "~> 1.15"
|
27
27
|
spec.add_development_dependency "rake", "~> 10.0"
|
28
28
|
spec.add_development_dependency "rspec", "~> 3.0"
|
29
|
+
spec.add_development_dependency "graphql", "~> 1.6"
|
30
|
+
spec.add_development_dependency "pry-byebug", "~> 3.4"
|
29
31
|
end
|
data/lib/batch_loader.rb
CHANGED
@@ -6,22 +6,20 @@ class BatchLoader
|
|
6
6
|
NoBatchError = Class.new(StandardError)
|
7
7
|
BatchAlreadyExistsError = Class.new(StandardError)
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
end
|
9
|
+
def self.for(item)
|
10
|
+
new(item: item)
|
11
|
+
end
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
end
|
13
|
+
def self.sync!(value)
|
14
|
+
case value
|
15
|
+
when Array
|
16
|
+
value.map! { |v| sync!(v) }
|
17
|
+
when Hash
|
18
|
+
value.each { |k, v| value[k] = sync!(v) }
|
19
|
+
when BatchLoader
|
20
|
+
sync!(value.sync)
|
21
|
+
else
|
22
|
+
value
|
25
23
|
end
|
26
24
|
end
|
27
25
|
|
data/lib/batch_loader/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: batch-loader
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- exAspArk
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-08-
|
11
|
+
date: 2017-08-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -52,6 +52,34 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: graphql
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.6'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.6'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry-byebug
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.4'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.4'
|
55
83
|
description: Simple tool to avoid N+1 DB queries, HTTP requests, etc.
|
56
84
|
email:
|
57
85
|
- exaspark@gmail.com
|