batch-loader 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0ced31b90f33da9d4e1fe719fe5f6f0b5a043e60
4
- data.tar.gz: bc75c0b4234c6bb01db28e648853be9afbc4e7bd
3
+ metadata.gz: e17f7617e248aa7b911c64dac882cdb192bb9b2c
4
+ data.tar.gz: 6b9d612ae58f95a0715c0d23427176375d8450c4
5
5
  SHA512:
6
- metadata.gz: f4548669975cacfac9958d50ddb7b8d4a4d7de1cb974dcefd7c4b1545987ccfa552d76dce04252fa26ffa5e03b6cded5d699232ece4e184a0cf870167ca3f009
7
- data.tar.gz: d3867a1d1313ae11f9d97af2b6dc3076165be4f68f990bf60487eaa7c811d2fad46356d909fc894537aea1992014636c5fe7c3b1127aabdc263ccde65f839fb5
6
+ metadata.gz: b34fa00da8e309c99b7bae84e8155dcd847722f5da30d60c19a318a114459d825645ef3377259219e5fa9c64c1ffd1495e4ac78bf4907ab811f66b8a8b0ba178
7
+ data.tar.gz: 7e97b3aa40becb1515a3a26d2a1992f2ccbfda2a4f5f090fc7c329e898319aa765edec379f8a0070d555276a0ab00cb24d9cf4235b230d0e7fcb6a0e0bce6625
@@ -2,4 +2,6 @@ sudo: false
2
2
  language: ruby
3
3
  rvm:
4
4
  - 2.3.4
5
+ env:
6
+ - CI=true
5
7
  before_install: gem install bundler -v 1.15.3
@@ -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.2.0...HEAD)
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`: More docs and tests.
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
@@ -2,7 +2,7 @@ source "https://rubygems.org"
2
2
 
3
3
  git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
4
 
5
- gem "pry"
5
+ gem 'coveralls', require: false
6
6
 
7
7
  # Specify your gem's dependencies in batch-loader.gemspec
8
8
  gemspec
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # BatchLoader
2
2
 
3
3
  [![Build Status](https://travis-ci.org/exAspArk/batch-loader.svg?branch=master)](https://travis-ci.org/exAspArk/batch-loader)
4
+ [![Coverage Status](https://coveralls.io/repos/github/exAspArk/batch-loader/badge.svg)](https://coveralls.io/github/exAspArk/batch-loader)
5
+ [![Code Climate](https://img.shields.io/codeclimate/github/exAspArk/batch-loader.svg)](https://codeclimate.com/github/exAspArk/batch-loader)
6
+ [![Downloads](https://img.shields.io/gem/dt/batch-loader.svg)](https://rubygems.org/gems/batch-loader)
7
+ [![Latest Version](https://img.shields.io/gem/v/batch-loader.svg)](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. Plus it'll preload the association every time, even if it's not necessary. Can we do better? Sure!
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 an object which in other similar implementations is call Promise. Each Promise knows which data it needs to load and how to batch the query. When all the Promises are collected it's possible to resolve them once without N+1 queries.
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
- def rating
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 for each request in the app add the following middleware:
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
- TODO
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
- TODO
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
- TODO
250
-
251
- ## Testing
252
-
253
- TODO
356
+ Coming soon
254
357
 
255
358
  ## Development
256
359
 
@@ -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
@@ -6,22 +6,20 @@ class BatchLoader
6
6
  NoBatchError = Class.new(StandardError)
7
7
  BatchAlreadyExistsError = Class.new(StandardError)
8
8
 
9
- class << self
10
- def for(item)
11
- new(item: item)
12
- end
9
+ def self.for(item)
10
+ new(item: item)
11
+ end
13
12
 
14
- def sync!(value)
15
- case value
16
- when Array
17
- value.map! { |v| sync!(v) }
18
- when Hash
19
- value.each { |k, v| value[k] = sync!(v) }
20
- when BatchLoader
21
- sync!(value.sync)
22
- else
23
- value
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
 
@@ -6,7 +6,11 @@ class BatchLoader
6
6
  Thread.current[NAMESPACE] ||= new
7
7
  end
8
8
 
9
- def self.delete_current
9
+ def self.current
10
+ Thread.current[NAMESPACE]
11
+ end
12
+
13
+ def self.clear_current
10
14
  Thread.current[NAMESPACE] = nil
11
15
  end
12
16
 
@@ -6,10 +6,9 @@ class BatchLoader
6
6
 
7
7
  def call(env)
8
8
  begin
9
- BatchLoader::Executor.ensure_current
10
9
  @app.call(env)
11
10
  ensure
12
- BatchLoader::Executor.delete_current
11
+ BatchLoader::Executor.clear_current
13
12
  end
14
13
  end
15
14
  end
@@ -1,3 +1,3 @@
1
1
  class BatchLoader
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
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.2.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-02 00:00:00.000000000 Z
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