README.md

# BatchLoader

This package provides a generic lazy batching mechanism to avoid N+1 DB queries, HTTP queries, etc.

## Contents

* [Highlights](#highlights)
* [Usage](#usage)
  * [With Absinthe (GraphQL)](#with-absinthe-graphql)
  * [With Ecto (DB)](#with-ecto-db)
  * [Customization](#customization)
* [Installation](#installation)
* [Testing](#testing)

## Highlights

* Generic utility to avoid N+1 DB queries, HTTP requests, etc.
* Adapted Elixir implementation of 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), etc.
* Allows inlining the code without forcing to define extra named functions (unlike [Absinthe Batch](https://hexdocs.pm/absinthe/Absinthe.Middleware.Batch.html)).
* Allows using batching with any data sources, not just `Ecto` (unlike [Absinthe DataLoader](https://hexdocs.pm/dataloader/Dataloader.html)).

## Usage

Let's imagine we have a `Post` GraphQL type:

```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 produces N+1 DB requests if we send this GraphQL request:

```gql
query {
  posts {
    title
    user { # N+1 request per each post
      name
    }
  }
}
```

### With Absinthe (GraphQL)

We can get rid of the N+1 requests by loading all `Users` for all `Posts` at once in.
All we have to do is to use `BatchLoader.Absinthe` in the `resolve` function:

```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
  Repo.all(from u in User, where: u.id in ^user_ids) # load all users at once (DB, HTTP, etc.)
  |> Enum.map(fn user -> {user.id, {:ok, user}} end) # return "{user.id, result}" tuples (where user.id == post.user_id)
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 ->
      Repo.all(from u in User, where: u.id in ^user_ids)
      |> Enum.map(fn user -> {user.id, {:ok, user}} end)
    end)
  end)
end
```

Finally, add `BatchLoader.Absinthe.Plugin` plugin to the Absinthe schema.
This will allow to lazily collect information about all users which need to be loaded and then load them all at once:

```elixir
defmodule MyApp.Schema do
  use Absinthe.Schema
  import_types MyApp.PostType

  def plugins do
    [BatchLoader.Absinthe.Plugin] ++ Absinthe.Plugin.defaults()
  end
end
```

### With Ecto (DB)

Set the default `repo` in your config file:

```elixir
# config/config.exs
config :batch_loader, :default_repo, MyApp.Repo
```

Now you can resolve Ecto associations with:

```elixir
field :user, :user_type, resolve: BatchLoader.Absinthe.resolve_assoc(:user)
```

To preload Ecto associations:

```elixir
field :title, :string do
  resolve(fn post, _, _ ->
    BatchLoader.Absinthe.preload_assoc(post, :user, fn post_with_user ->
      {:ok, "#{post_with_user.title} - #{post_with_user.user.name}"}
    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, fn post_with_user -> _ end, 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, fn post_with_user -> _ end, 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.3"}
  ]
end
```

## Testing

```ex
make install
make test
```