Created
March 10, 2017 06:52
-
-
Save mjason/128f4f971360469d0ec8ecbda46dac19 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Absinthe.Ecto do | |
import Absinthe.Resolution.Helpers | |
@moduledoc """ | |
Provides some helper functions for easy batching of ecto assocations | |
These functions all make use of the batch plugin found in Absinthe, they're | |
merely just some helpful ways to use this plugin in the context of simple ecto | |
associations. | |
## Basic Usage | |
First specify the repo you're going to use: | |
```elixir | |
use Absinthe.Ecto, repo: MyApp.Repo | |
``` | |
Then, supposing you have some ecto associations as in this example schema: | |
```elixir | |
defmodule MyApp.Post do | |
use Ecto.Schema | |
schema "posts" do | |
belongs_to :author, MyApp.User | |
has_many :comments, MyApp.Comment | |
field :name, :string | |
field :body, :string | |
end | |
end | |
``` | |
Your graphql post object might look like: | |
```elixir | |
object :post do | |
field :author, :user, resolve: assoc(:author) | |
field :comments, list_of(:comment), resolve: assoc(:comments) | |
field :title, :string | |
field :body, :string | |
end | |
``` | |
Now, queries which get the author or comments of many posts will result in | |
just 1 call to the database for each! | |
The `assoc` macro just builds a resolution function which calls `ecto_batch/4`. | |
See the `ecto_batch/4` function for how to do this from within a regular | |
resolution function. | |
""" | |
defmacro __using__([repo: repo]) do | |
quote do | |
import unquote(__MODULE__), only: [ | |
assoc: 1, | |
assoc: 2, | |
ecto_batch: 3, | |
ecto_batch: 4, | |
ecto_batch_opts: 4, | |
ecto_batch_opts: 5 | |
] | |
@__absinthe_ecto_repo__ unquote(repo) | |
end | |
end | |
@doc false | |
def __check_absinthe_ecto_repo__(nil), do: raise """ | |
You must `use Absinthe.Ecto, repo: MyApp.Repo` with your application's repo. | |
""" | |
@doc false | |
def __check_absinthe_ecto_repo__(_), do: nil | |
@doc """ | |
Example: | |
```elixir | |
field :author, :user, resolve: assoc(:author) | |
``` | |
""" | |
defmacro assoc(association, query \\ nil) do | |
quote do | |
# silent `warning: this check/guard will always yield the same result` | |
unquote(__MODULE__).__check_absinthe_ecto_repo__(@__absinthe_ecto_repo__) | |
unquote(__MODULE__).assoc_options(@__absinthe_ecto_repo__, unquote(association), unquote(query)) | |
end | |
end | |
defp default_callback(result) do | |
{:ok, result} | |
end | |
@doc """ | |
Generally you would use the `assoc/1` macro. | |
However, this can be useful if you need to specify an ecto repo. | |
```elixir | |
field :author, :user, resolve: assoc(MyApp.Repo, :author) | |
``` | |
""" | |
def assoc_options(repo, association, query) when query != nil do | |
fn parent, _, _ -> | |
case Map.get(parent, association) do | |
%Ecto.Association.NotLoaded{} -> | |
ecto_batch_opts(repo, parent, association, query) | |
val -> | |
{:ok, val} | |
end | |
end | |
end | |
def assoc_options(repo, association, _opts) do | |
fn parent, _, _ -> | |
case Map.get(parent, association) do | |
%Ecto.Association.NotLoaded{} -> | |
ecto_batch(repo, parent, association) | |
val -> | |
{:ok, val} | |
end | |
end | |
end | |
@doc """ | |
This function lets you batch load an item from within a normal resolution function. | |
It also supports a callback which is run after the item is loaded. For belongs | |
to associations this may be nil. | |
## Example | |
resolve fn post, _, _ -> | |
MyApp.Repo |> ecto_batch(post, :author, fn author -> | |
# you can do something with the author after its loaded here. | |
# note that it may be nil. | |
{:ok, author} | |
end) | |
end | |
""" | |
def ecto_batch(repo, %model{} = parent, association, callback \\ &default_callback/1) do | |
assoc = model.__schema__(:association, association) | |
%{owner: owner, | |
owner_key: owner_key, | |
field: field} = assoc | |
id = Map.fetch!(parent, owner_key) | |
meta = {repo, owner, owner_key, field, self()} | |
batch({__MODULE__, :perform_batch, meta}, id, fn results -> | |
results | |
|> Map.get(id) | |
|> callback.() | |
end) | |
end | |
def ecto_batch_opts(repo, %model{} = parent, association, query, callback \\ &default_callback/1) do | |
assoc = model.__schema__(:association, association) | |
%{owner: owner, | |
owner_key: owner_key, | |
field: field} = assoc | |
id = Map.fetch!(parent, owner_key) | |
meta = {repo, owner, owner_key, field, query, self()} | |
batch({__MODULE__, :perform_batch_opts, meta}, id, fn results -> | |
results | |
|> Map.get(id) | |
|> callback.() | |
end) | |
end | |
@doc false | |
# this has to be public because it gets called from the absinthe batcher | |
def perform_batch_opts({repo, owner, owner_key, field, query, caller}, ids) do | |
unique_ids = ids |> MapSet.new |> MapSet.to_list | |
options = Keyword.put([], field, query) | |
unique_ids | |
|> Enum.map(&Map.put(struct(owner), owner_key, &1)) | |
|> repo.preload(options, caller: caller) | |
|> Enum.map(&{Map.get(&1, owner_key), Map.get(&1, field)}) | |
|> Map.new | |
end | |
def perform_batch({repo, owner, owner_key, field, caller}, ids) do | |
unique_ids = ids |> MapSet.new |> MapSet.to_list | |
unique_ids | |
|> Enum.map(&Map.put(struct(owner), owner_key, &1)) | |
|> repo.preload(field, caller: caller) | |
|> Enum.map(&{Map.get(&1, owner_key), Map.get(&1, field)}) | |
|> Map.new | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment