Skip to content

Commit

Permalink
Release v1.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
exAspArk committed Nov 2, 2017
1 parent 4735ffa commit 3105f95
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 65 deletions.
10 changes: 7 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ one of the following labels: `Added`, `Changed`, `Deprecated`,
to manage the versions of this gem so
that you can set version constraints properly.

#### [Unreleased](https://github.com/exAspArk/batch-loader/compare/v1.0.4...HEAD)
#### [Unreleased](https://github.com/exAspArk/batch-loader/compare/v1.1.0...HEAD)

* `Added`: `default_value` override option.
* `Added`: `loader.call {}` block syntax, for memoizing repeat calls to the same item.
* WIP

#### [v1.1.0](https://github.com/exAspArk/batch-loader/compare/v1.0.4...v1.1.0)

* `Added`: `default_value` override option. [#8](https://github.com/exAspArk/batch-loader/pull/8)
* `Added`: `loader.call {}` block syntax, for memoizing repeat calls to the same item. [#8](https://github.com/exAspArk/batch-loader/pull/8)

#### [v1.0.4](https://github.com/exAspArk/batch-loader/compare/v1.0.3...v1.0.4)

Expand Down
70 changes: 23 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ This gem provides a generic lazy batching mechanism to avoid N+1 DB queries, HTT
* [Highlights](#highlights)
* [Usage](#usage)
* [Why?](#why)
* [Basic examples](#basic-examples)
* [Basic example](#basic-example)
* [How it works](#how-it-works)
* [RESTful API example](#restful-api-example)
* [GraphQL example](#graphql-example)
* [Loading multiple items](#loading-multiple-items)
* [Caching](#caching)
* [Installation](#installation)
* [Implementation details](#implementation-details)
Expand Down Expand Up @@ -97,7 +98,7 @@ puts users # Users

But the problem here is that `load_posts` now depends on the child association and knows that it has to preload data for future use. And it'll do it every time, even if it's not necessary. Can we do better? Sure!

### Basic examples
### Basic example

With `BatchLoader` we can rewrite the code above:

Expand Down Expand Up @@ -125,51 +126,6 @@ puts users # Users SELECT * FROM users WHERE id IN

As we can see, batching is isolated and described right in a place where it's needed.

For batches where there is no item in response to a call, we normally return nil. However, you can use `default_value:` to return something else instead. This is particularly useful for 1:Many relationships, where you

```ruby
def load_posts(ids)
Post.where(id: ids)
end

def load_user(post)
BatchLoader.for(post.user_id).batch(default_value: NullUser.new) do |user_ids, loader|
User.where(id: user_ids).each { |user| loader.call(user.id, user) }
end
end

posts = load_posts([1, 2, 3])


users = posts.map do |post|
load_user(post)
end

puts users
```

For batches where the value is some kind of collection, such as an Array or Hash, `loader` also supports being called with a block, which yields the _current_ value, and returns the _next_ value. This is extremely useful for 1:Many relationships:

```ruby
def load_users(ids)
User.where(id: ids)
end

def load_comments(user)
BatchLoader.for(user.id).batch(default_value: []) do |comment_ids, loader|
Comment.where(user_id: user_ids).each do |comment|
loader.call(user.id) { |memo| memo.push(comment) }
end
end
end

users = load_users([1, 2, 3])

comments = users.map do |user|
load_comments(user)
end
```

### How it works

In general, `BatchLoader` returns a lazy object. Each lazy object knows which data it needs to load and how to batch the query. As soon as you need to use the lazy objects, they will be automatically loaded once without N+1 queries.
Expand Down Expand Up @@ -324,6 +280,26 @@ end

That's it.

### Loading multiple items

For batches where there is no item in response to a call, we normally return `nil`. However, you can use `:default_value` to return something else instead:

```ruby
BatchLoader.for(post.user_id).batch(default_value: NullUser.new) do |user_ids, loader|
User.where(id: user_ids).each { |user| loader.call(user.id, user) }
end
```

For batches where the value is some kind of collection, such as an Array or Hash, `loader` also supports being called with a block, which yields the _current_ value, and returns the _next_ value. This is extremely useful for 1:Many relationships:

```ruby
BatchLoader.for(user.id).batch(default_value: []) do |comment_ids, loader|
Comment.where(user_id: user_ids).each do |comment|
loader.call(user.id) { |memo| memo << comment }
end
end
```

### Caching

By default `BatchLoader` caches the loaded values. You can test it by running something like:
Expand Down
14 changes: 7 additions & 7 deletions lib/batch_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ class BatchLoader
IMPLEMENTED_INSTANCE_METHODS = %i[object_id __id__ __send__ singleton_method_added __sync respond_to? batch inspect].freeze
REPLACABLE_INSTANCE_METHODS = %i[batch inspect].freeze
LEFT_INSTANCE_METHODS = (IMPLEMENTED_INSTANCE_METHODS - REPLACABLE_INSTANCE_METHODS).freeze
NULL_VALUE = :batch_loader_null

NoBatchError = Class.new(StandardError)

Expand Down Expand Up @@ -77,13 +76,14 @@ def __ensure_batched
return if __executor_proxy.value_loaded?(item: @item)

items = __executor_proxy.list_items
loader = -> (item, value = NULL_VALUE, &block) {
if block
raise ArgumentError, "Please pass a value or a block, not both" if value != NULL_VALUE
next_value = block.call(__executor_proxy.loaded_value(item: item))
else
next_value = value
loader = -> (item, value = (no_value = true; nil), &block) {
if no_value && !block
raise ArgumentError, "Please pass a value or a block"
elsif block && !no_value
raise ArgumentError, "Please pass a value or a block, not both"
end

next_value = block ? block.call(__executor_proxy.loaded_value(item: item)) : value
__executor_proxy.load(item: item, value: next_value)
}

Expand Down
2 changes: 1 addition & 1 deletion lib/batch_loader/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

class BatchLoader
VERSION = "1.0.4"
VERSION = "1.1.0"
end
20 changes: 13 additions & 7 deletions spec/batch_loader_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,22 @@
expect(lazy).to eq([1,2,3])
end

context "called with block and value syntax" do
it 'raises ArgumentError' do
lazy = BatchLoader.for(1).batch(default_value: {}) do |nums, loader|
nums.each do |num|
loader.call(num, "one value") { "too many values" }
end
it 'raises ArgumentError if called with block and value' do
lazy = BatchLoader.for(1).batch do |nums, loader|
nums.each do |num|
loader.call(num, "one value") { "too many values" }
end
end

expect { lazy.sync }.to raise_error(ArgumentError)
expect { lazy.sync }.to raise_error(ArgumentError)
end

it 'raises ArgumentError if called without block and value' do
lazy = BatchLoader.for(1).batch do |nums, loader|
nums.each { |num| loader.call(num) }
end

expect { lazy.sync }.to raise_error(ArgumentError)
end
end

Expand Down

0 comments on commit 3105f95

Please sign in to comment.