dataloader v2.0.2 Dataloader.Ecto

Ecto source for Dataloader

This defines a schema and an implementation of the Dataloader.Source protocol for handling Ecto related batching.

A simple Ecto source only needs to know about your application’s Repo.

Basic Usage

source = Dataloader.Ecto.new(MyApp.Repo)

loader =
  Dataloader.new
  |> Dataloader.add_source(Accounts, source)
  |> Dataloader.load(Accounts, User, 1)
  |> Dataloader.load_many(Accounts, Organization, [4, 9])
  |> Dataloader.run

organizations = Dataloader.get(loader, Accounts, Organization, [4,9])

loader =
  loader
  |> Dataloader.load_many(Accounts, :users, organizations)
  |> Dataloader.run

Filtering / Ordering

Dataloader.new/2 can receive a 2 arity function that can be used to apply broad ordering and filtering rules, as well as handle parameters

source = Dataloader.Ecto.new(MyApp.Repo, query: &Accounts.query/2)

loader =
  Dataloader.new
  |> Dataloader.add_source(Accounts, source)

When we call load/4 we can pass in a tuple as the batch key

loader
|> Dataloader.load(Accounts, {User, order: :name}, 1)

# or
loader
|> Dataloader.load_many(Accounts, {:users, order: :name}, organizations)

# this is still supported
loader
|> Dataloader.load(Accounts, User, 1)

# as is this
loader
|> Dataloader.load(:accounts, :user, :organization)

In all cases the Accounts.query function would be:

def query(User, params) do
  field = params[:order] || :id
  from u in User, order_by: [asc: field(u, ^field)]
end
def query(queryable, _) do
  queryable
end

If we query something that ends up using the User schema, whether directly or via association, the query/2 function will match on the first clause and we can handle the params. If no params are supplied, the params arg defaults to source.default_params which itself defaults to %{}.

default_params is an extremely useful place to store values like the current user:

source = Dataloader.Ecto.new(MyApp.Repo, [
  query: &Accounts.query/2,
  default_params: %{current_user: current_user},
])

loader =
  Dataloader.new
  |> Dataloader.add_source(Accounts, source)
  |> Dataloader.load_many(Accounts, Organization, ids)
  |> Dataloader.run

# the query function
def query(Organization, %{current_user: user}) do
  from o in Organization,
    join: m in assoc(o, :memberships),
    where: m.user_id == ^user.id
end
def query(queryable, _) do
  queryable
end

In our query function we are pattern matching on the current user to make sure that we are only able to lookup data in organizations that the user actually has a membership in. Additional options you specify IE {Organization, %{order: :asc}} are merged into the default.

Link to this section Summary

Functions

Create an Ecto Dataloader source

Default implementation for loading a batch. Handles looking up records by column

Link to this section Types

Link to this type batch_fun()
batch_fun() :: (Ecto.Queryable.t(), Ecto.Query.t(), [any()], Keyword.t() -> [any()])
Link to this type opt()
opt() ::
  {:query, query_fun()} |
  {:repo_opts, Keyword.t()} |
  {:timeout, pos_integer()}
Link to this type query_fun()
query_fun() :: (Ecto.Queryable.t(), any() -> Ecto.Queryable.t())
Link to this type t()
t() :: %Dataloader.Ecto{batches: map(), default_params: map(), name: term(), options: Keyword.t(), query: query_fun(), repo: Ecto.Repo.t(), repo_opts: Keyword.t(), results: map(), run_batch: batch_fun()}

Link to this section Functions

Link to this function new(repo, opts \\ [])
new(Ecto.Repo.t(), [opt()]) :: t()

Create an Ecto Dataloader source.

This module handles retrieving data from Ecto for dataloader. It requires a valid Ecto Repo. It also accepts a repo_opts: option which is handy for applying options to any calls to Repo functions that this module makes.

For example, you can use this module in a multi-tenant context by using the prefix option:

Dataloader.Ecto.new(MyApp.Repo, repo_opts: [prefix: "tenant"])
Link to this function run_batch(repo, queryable, query, col, inputs, repo_opts)

Default implementation for loading a batch. Handles looking up records by column