Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dataloader helper for custom run_batch #903

Closed
Gazler opened this issue Apr 10, 2020 · 10 comments
Closed

Dataloader helper for custom run_batch #903

Gazler opened this issue Apr 10, 2020 · 10 comments

Comments

@Gazler
Copy link
Contributor

Gazler commented Apr 10, 2020

Currently when using a custom run_batch/5 function such as the one from the docs:

https://hexdocs.pm/dataloader/Dataloader.Ecto.html#module-custom-batch-queries

def run_batch(_, query, :post_count, users, repo_opts) do
  user_ids = Enum.map(users, & &1.id)
  default_count = 0

  result =
    query
    |> where([p], p.user_id in ^user_ids)
    |> group_by([p], p.user_id)
    |> select([p], {p.user_id, count("*")})
    |> Repo.all(repo_opts)
    |> Map.new()

  for %{id: id} <- users do
    [Map.get(result, id, default_count)]
  end
end

# Fallback to original run_batch
def run_batch(queryable, query, col, inputs, repo_opts) do
  Dataloader.Ecto.run_batch(Repo, queryable, query, col, inputs, repo_opts)
end

# Returns 2
Dataloader.get(loader, Posts, {:one, Post}, post_count: user1)

In order to execute the from a resolver, currently, it requires a resolver like:

def post_count(user, args, %{context: %{loader: loader} = context}) do
  loader
  |> Dataloader.load(Posts, {:one, Post}, post_count: user)
  |> on_load(fn loader ->
    result = Dataloader.get(Posts, {:one, Post}, post_count: user)
    {:ok, result}
  end)
end

It is possible today to us a helper like this for resolving:

resolve dataloader(Posts, fn _user, _args, _ ->
  {:one, Post}
end)

However there is no way to express the required item_key for the dataloader.

Perhaps the dataloader response could be altered to permit this. Maybe something like:

resolve dataloader(Posts, fn user, _args, _ ->
 {{:one, Post}, post_count: user}
end)
@benwilson512
Copy link
Contributor

I think I'd like to avoid a tuple based approach, Dataloader already has a lot of tuple formats. Maybe a map? eg:

%{key: {:one, Post}, value: [post_count: user]}

@Gazler
Copy link
Contributor Author

Gazler commented Apr 10, 2020

I'm not against this, I agree tuples can be confusing, especially when you include the optional arguments that can be passed ({:one, Post, %{published: true}}). Is key and value the correct terminology? They are technically both keys. Maybe we could use: batch_key and item_key?

@benwilson512
Copy link
Contributor

@Gazler good point, maybe simply %{batch: batch, item: item} ?

@benwilson512
Copy link
Contributor

Fixed per PR

@BlueHotDog
Copy link

Hey, just tried that and i'm super confused, whats the final syntax? where is it documented?

@maartenvanvliet
Copy link
Contributor

maartenvanvliet commented Jun 15, 2020

Given the example in the first post the following should work with the new syntax:

# your object definition
resolve dataloader(Posts, fn user, _args, _ ->
 %{batch: {{:one, Post}, %{}}, item: [post_count: user]}
end)

The empty map passed in the tuple for the batch key is a map of arguments.

@VictorGaiva
Copy link

VictorGaiva commented Nov 5, 2021

Is there a way to make the absinthe context accessible from run_batch? It would allow, for example, to batch the resolution of a boolean field that checks if the current user is subscribed to a post.

@benwilson512
Copy link
Contributor

@VictorGaiva data about the current user should be passed in as part of the args tuple. eg:

resolve dataloader(Posts, fn user, _args, %{context: %{current_user: user}} ->
 %{batch: {{:one, Post}, %{user: user}}, item: [post_count: user]}
end)

@VictorGaiva
Copy link

VictorGaiva commented Nov 5, 2021

@benwilson512 %{user: user} doesn't seem to be passed to &run_batch/5.

With:

object :product do
  field :following, non_null(:boolean), default_value: false do
    resolve(
      dataloader(Db, fn product, _args, %{context: %{current_user: user}} ->
        %{batch: {{:one, Core.User}, %{user: user}}, item: [following: product]}
      end)
    )
  end
end

and

def run_batch(a, b, :following, products, c) do
  IO.inspect(a)
  IO.inspect(b)
  IO.inspect(c)
  # result = Products.followed_by() |> Enum.map(& &1.id)

  for %{id: _id} <- products do
    [false]
  end
end

prints:

Core.User
Core.User
[caller: #PID<0.1841.0>, prefix: "public"]

And products is a %Core.Product{} array.

@VictorGaiva
Copy link

Was able to do what I wanted, inspired by this forum post, like this:

With:

object :product do
  field :following, non_null(:boolean), default_value: false do
    resolve(
      dataloader(Db, fn product, _args, %{context: %{current_user: user}} ->
        %{batch: {:following, user}, item: product}
      end)
    )
  end
end

and

def load({:following, user}, products) do
  result = Products.followed_by(user) |> Enum.map(& &1.id)

  products
  |> Map.new(fn %{id: id} = product ->
    {product, Enum.member?(result, id)}
  end)
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants