Hi All,
I have these objects in graphql scheme:
import Absinthe.Resolution.Helpers
object :user do
field :uuid, :id
field :email, non_null(:string)
end
object :asset do
field :uuid, :id
field :name, non_null(:string)
field :user, non_null(:user) do
resolve dataloader(Auth, :user)
end
end
def context(ctx) do
loader =
Dataloader.new
|> Dataloader.add_source(Auth, Auth.dataloader_source())
Map.put(ctx, :loader, loader)
end
def plugins do
[Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults()
end
where Auth
is Phoenix context:
defmodule App.Auth do
@moduledoc """
The Auth context.
"""
import Ecto.Query, warn: false
def dataloader_source() do
Dataloader.Ecto.new(Repo, query: &query/2)
end
def query(queryable, _params) do
queryable
end
...
And this mutation:
field :create_asset, type: :asset do
arg :name, non_null(:string)
resolve &Resolvers.create_asset/3
end
And these resolver functions:
def create_asset(_parent, args, %{context: %{current_user: user}}) do
case Accounting.create_asset(Map.put(args, :user, user)) do
{:ok, asset} -> {:ok, asset}
{:error, %Ecto.Changeset{} = cs} ->
{:error, message: Helpers.format_changeset_errors(cs)}
end
end
def create_asset(_parent, _args, _context) do
{:error, "not authorized"}
end
Accounting.create_asset
is something like:
def create_asset(attrs \\ %{}) do
%Asset{}
|> ...
|> put_assoc(:user, attrs.user)
|> Repo.insert()
end
So Accounting.create_asset
accepts user structure instead of user_id.
That means at the moment asset is successfully created asset.user
is loaded (meta: #Ecto.Schema.Metadata<:loaded, “users”>,).
But when graphql server is processing mutation like:
mutation createAsset {
createAsset (name: "foobar") {
uuid
name
user {
email
uuid
}
}
}
three database query is executed:
- get current user from auth token. -> ok
- insert asset. -> ok
- get asset user (to resolve field
user
in mutation). -> ?
I suppose the last query should not be executed, becase at the time
user
field is resolved it’s already loaded to parent (asset) structire.
I tried this approach.
Instead of:
object :asset do
field :uuid, :id
field :name, non_null(:string)
field :user, non_null(:user) do
resolve dataloader(Auth, :user)
# resolve &Resolvers.get_related_user/3
end
end
I do (change resolver function to custom one):
object :asset do
field :uuid, :id
field :name, non_null(:string)
field :user, non_null(:user) do
resolve &Resolvers.get_related_user/3
end
end
where Resolvers.get_related_user/3
is:
def get_related_user(parent, args, context) do
case parent.user do
%Ecto.Association.NotLoaded{} ->
dataloader(Auth, :user).(parent, args, context)
_ ->
{:ok, parent.user}
end
end
It works. Now there is only 2 db requests. But I believe it’s ugly approach and there is definitely some nicer way to let Dataloader know that user object is already loaded.
Can you please give an advice?