# BatchLoader
[](https://travis-ci.org/exAspArk/batch_loader)
[](https://coveralls.io/github/exAspArk/batch_loader)
[](https://hex.pm/packages/batch_loader)
This package provides a generic lazy batching mechanism to avoid N+1 DB queries, HTTP queries, etc.
## Contents
* [Highlights](#highlights)
* [Usage](#usage)
* [Ecto Resolve Association](#ecto-resolve-association)
* [Ecto Load Association](#ecto-load-association)
* [Ecto Preload Association](#ecto-preload-association)
* [DIY Batching](#diy-batching)
* [Customization](#customization)
* [Installation](#installation)
* [Testing](#testing)
## Highlights
* Generic utility to avoid N+1 DB queries, HTTP requests, etc.
* Adapted Elixir implementation of the battle-tested tools like [Haskell Haxl](https://github.com/facebook/Haxl), [JS DataLoader](https://github.com/graphql/dataloader), [Ruby BatchLoader](https://github.com/exaspark/batch-loader).
* Convenient and flexible integration with Ecto Schemas.
* Allows inlining the code without defining extra named functions, unlike [Absinthe Batch](https://hexdocs.pm/absinthe/Absinthe.Middleware.Batch.html).
* Allows using batching with any data sources, not just DB, unlike [Absinthe DataLoader](https://hexdocs.pm/dataloader/Dataloader.html).
## Usage
Let's imagine that we have a `Post` GraphQL type defined with [Absinthe](https://github.com/absinthe-graphql/absinthe):
```elixir
defmodule MyApp.PostType do
use Absinthe.Schema.Notation
alias MyApp.Repo
object :post_type do
field :title, :string
field :user, :user_type do
resolve(fn post, _, _ ->
user = post |> Ecto.assoc(:user) |> Repo.one() # N+1 DB requests
{:ok, user}
end)
end
end
end
```
This will produce N+1 DB requests if we send this GraphQL request:
```gql
query {
posts {
title
user { # N+1 request per each post
name
}
}
}
```
### Ecto Resolve Association
We can get rid of the N+1 DB requests by loading all `Users` for all `Posts` at once in.
All we have to do is to use `resolve_assoc` function by passing the Ecto associations name:
```elixir
import BatchLoader.Absinthe, only: [resolve_assoc: 1]
field :user, :user_type, resolve: resolve_assoc(:user)
```
Set the default `repo` in your `config.exs` file:
```elixir
config :batch_loader, :default_repo, MyApp.Repo
```
And finally, add `BatchLoader.Absinthe.Plugin` plugin to the GraphQL schema.
This will allow to lazily collect information about all users which need to be loaded and then batch them all together:
```elixir
defmodule MyApp.Schema do
use Absinthe.Schema
import_types MyApp.PostType
def plugins do
[BatchLoader.Absinthe.Plugin] ++ Absinthe.Plugin.defaults()
end
end
```
### Ecto Load Association
You can use `load_assoc` to load Ecto associations in the existing schema:
```elixir
import BatchLoader.Absinthe, only: [load_assoc: 3]
field :author, :string do
resolve(fn post, _, _ ->
load_assoc(post, :user, fn user ->
{:ok, user.name}
end)
end)
end
```
### Ecto Preload Association
You can use `preload_assoc` to preload Ecto associations in the existing schema:
```elixir
import BatchLoader.Absinthe, only: [preload_assoc: 3]
field :title, :string do
resolve(fn post, _, _ ->
preload_assoc(post, :user, fn post_with_user ->
{:ok, "#{post_with_user.title} - #{post_with_user.user.name}"}
end)
end)
end
```
### DIY Batching
You can also use `BatchLoader` to batch in the `resolve` function manually, for example, to fix N+1 HTTP requests:
```elixir
field :user, :user_type do
resolve(fn post, _, _ ->
BatchLoader.Absinthe.for(post.user_id, &resolved_users_by_user_ids/1)
end)
end
def resolved_users_by_user_ids(user_ids) do
MyApp.HttpClient.users(user_ids) # load all users at once
|> Enum.map(fn user -> {user.id, {:ok, user}} end) # return "{user.id, result}" tuples
end
```
Alternatively, you can simply inline the batch function:
```elixir
field :user, :user_type do
resolve(fn post, _, _ ->
BatchLoader.Absinthe.for(post.user_id, fn user_ids ->
MyApp.HttpClient.users(user_ids)
|> Enum.map(fn user -> {user.id, {:ok, user}} end)
end)
end)
end
```
### Customization
* To specify default resolve Absinthe values:
```elixir
BatchLoader.Absinthe.for(post.user_id, &resolved_users_by_user_ids/1, default_value: {:error, "NOT FOUND"})
```
* To use custom callback function:
```elixir
BatchLoader.Absinthe.for(post.user_id, &users_by_user_ids/1, callback: fn user ->
{:ok, user.name}
end)
```
* To use custom Ecto repos:
```elixir
BatchLoader.Absinthe.resolve_assoc(:user, repo: AnotherRepo)
BatchLoader.Absinthe.preload_assoc(post, :user, &callback/1, repo: AnotherRepo)
```
* To pass custom options to `Ecto.Repo.preload`:
```elixir
BatchLoader.Absinthe.resolve_assoc(:user, preload_opts: [prefix: nil])
BatchLoader.Absinthe.preload_assoc(post, :user, &callback/1, preload_opts: [prefix: nil])
```
## Installation
Add `batch_loader` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:batch_loader, "~> 0.1.0-beta.6"}
]
end
```
## Testing
```ex
make install
make test
```