-
Notifications
You must be signed in to change notification settings - Fork 36
Description
Hi, I've been using Absinthe and Absinthe.Relay intensively for a private project and wrote a few helper functions in order to resolve GraphQL queries into Ecto queries.
I've put the code here: https://gist.github.com/redrabbit/be3528a4e4479886acbe648a693e65c0
I don't know if Absinthe.Ecto is going to be implemented in a similar manner. If there is interest, I can write a PR and add some tests to it.
Basically, it uses Ecto.Schema and Absinthe.Resolution to resolve pretty much every kind of GraphQL query I'd came across so far.
There are two distinct functions, build_query/3 and batch_query/5.
By default, build_query/3 automatically resolves :join, :preload and :select clauses. It tries to be as performant as possible and selects only required fields.
Setup
Let's say we have schemas representing a music library:
schema "artists" do
field :name, :string
has_many :albums, Album
has_many :tracks, Track
timestamps
end
schema "albums" do
field :title, :string
field :release_date, Ecto.Date
belongs_to :artist, Artist
many_to_many :genres, Genre, join_through: "albums_genres"
has_many :tracks, Track
timestamps
end
schema "genres" do
field :name, :string
many_to_many :albums, Album, join_through: "albums_genres"
timestamps
end
schema "tracks" do
field :title, :string
field :track_nr, :integer
field :duration, :integer
field :popularity, :integer
belongs_to :artist, Artist
belongs_to :album, Album
timestamps
endAnd the following matching GraphQL types:
object :artist do
field :id, :id
field :name, :string
field :albums, list_of(:album)
field :tracks, list_of(:track) do
arg :top, :integer, default_value: 10
resolve &resolve_artist_top_tracks/3
end
end
object :album do
field :id, :id
field :title, :string
field :release_date, :date
field :artist, :artist
field :tracks, list_of(:track)
field :genres, list_of(:genre)
end
object :genre do
field :id, :id
field :name, :string
field :albums, list_of(:album)
end
object :track do
field :id, :id
field :title, :string
field :artist, :artist
field :album, :album
field :track_nr, :integer
field :duration, :integer
field :popularity, :integer
endUsage
Following GraphQL query:
{
artists {
name
albums {
title
tracks {
title
}
genres {
name
}
}
}
}
Returns a single Ecto.Query:
#Ecto.Query<from a0 in Artist,
join: a1 in assoc(a0, :albums),
join: g in assoc(a1, :genres),
join: t in assoc(a1, :tracks),
select: [:id, :name, {:albums, [:id, :title, {:genres, [:id, :name]}, {:tracks, [:id, :title]}]}],
preload: [albums: {a1, [genres: g, tracks: t]}]>
It handles :belongs_to, :has_one, :has_many and :many_to_many associations and will try to join and preload as much as possible. It also support resolving cyclic graphs.
Writing resolvers is straight forward:
def resolve_artists(_args, info) do
query = build_query(Artist, info, &order_by(&1, [asc: :name]))
{:ok, Repo.all(query)}
end
def resolve_artist_by_id(%{id: id}, info) do
id = String.to_integer(id)
query = build_query(Artist, info, &where(&1, [id: ^id]))
case Repo.one(query) do
nil ->
{:error, "Cannot find artist with id #{id}."}
artist ->
{:ok, artist}
end
endIn order to support batching and avoid N+1 queries, batch_query/5 provides similar functionalities and resolve the given associations automatically. For example:
object :artist do
field :id, :id
field :name, :string
field :albums, list_of(:album)
field :tracks, list_of(:track) do
arg :top, :integer, default_value: 10
resolve fn artist, args, info ->
batch_query(Track, artist, info, &Repo.all/1, fn query ->
query
|> order_by([desc: :popularity])
|> limit(^min(Map.get(args, :top, 10), 100))
end)
end
end
endIn the above object, the :albums field is resolved automatically within the query using a :join. The :tracks field will require a 2nd query (using where: [artist_id: id] or where: a1.artist_id in ^ids).
Resulting in executing two SQL queries for the following GraphQL:
{
artists {
name
tracks(top:5) {
title,
duration
}
albums {
title
tracks {
title
}
genres {
name
}
}
}
}
2016-11-26 02:57:28.093 [debug] QUERY OK source="artists" db=2.1ms
SELECT a0."id", a0."name", a1."id", a1."title", t2."id", t2."title", g3."id", g3."name" FROM "artists" AS a0 INNER JOIN "albums" AS a1 ON a1."artist_id" = a0."id" INNER JOIN "tracks" AS t2 ON t2."album_id" = a1."id" INNER JOIN "albums_genres" AS a4 ON a4."album_id" = a1."id" INNER JOIN "genres" AS g3 ON a4."genre_id" = g3."id" ORDER BY a0."name" []
2016-11-26 02:57:28.095 [debug] QUERY OK source="tracks" db=1.6ms
SELECT t0."artist_id", t0."id", t0."title", t0."duration" FROM "tracks" AS t0 WHERE (t0."artist_id" = $1) ORDER BY t0."popularity" DESC LIMIT $2 [1, 5]
Customization
You can customize your queries using the optional transform parameter both functions provide.
For example, to fetch albums sorted by :release_date and album tracks sorted by :track_nr, one can write two new resolver functions:
def resolve_artist_albums(artist, _args, info) do
batch_query(Album, artist, info, &order_by(&1, [desc: :release_date]))
end
def resolve_album_tracks(album, _args, info) do
batch_query(Track, album, info, &order_by(&1, [asc: :track_nr]))
endAnd update the schema types like this:
object :artist do
field :albums, list_of(:album), do: resolve &resolve_artist_albums/3
end
object :album do
field :tracks, list_of(:track), do: resolve &resolve_album_tracks/3
end