はじめに
ElixirのWebフレームワークPhoenixでは、Absintheというライブラリを用いることでGraphQLのSchemaやTypeなどを簡単に定義、実装することができます。
しかしGraphQLでPaginationを実装する上で必要となるConnectionの実装方法については日本語のドキュメントがあまりなかったのでまとめておこうと思います。
せっかくなのでAbsintheの導入からまとめていきます。(Mutationの実装は行いません)
ちなみにこの記事ではGraphQLに対しての説明はしないので、GraphQLがなんなのかについて知りたい場合は以下のサイトなどを参考にしてください。
Introduction GraphQL
環境
Elixir 1.7.3
Phoenix 1.4.0
前提として以下のようなContextに対して実装を行なっていきます
defmodule BlogApp.Blogs do
@moduledoc """
The Blogs context.
"""
import Ecto.Query, warn: false
alias BlogApp.Repo
alias BlogApp.Blogs.Post
def list_posts_query do
Post
|> order_by(desc: :inserted_at)
end
def get_post(id), do: Repo.get(Post, id)
end
defmodule BlogApp.Blogs.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :body, :string
field :image, :string
field :title, :string
timestamps()
end
@doc false
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :body, :image])
|> validate_required([:title, :body])
end
end
ライブラリのインストール
mix.exs
のdeps
に以下のライブラリを追加します。
{:absinthe, "~> 1.4.0"},
{:absinthe_plug, "~> 1.4.0"},
{:poison, "~> 3.1.0"},
{:absinthe_relay, "~> 1.4"},
absinthe
とabsinthe_plug
, poison
は基本的な実装に必要なもので,absinthe_relay
はGraphQL Connectionを定義するのに必要なライブラリです。
追加を行なったらmix deps.get
を実行します。
これでライブラリの準備は完了です。
Typesの実装
ここから実際にGraphQLの実装を行なっていきます。
GraphQLの実装は/lib/blog_app_web/blogs/
以下に行なっていきます。
はじめにTypesの実装をします。実装はPostと PostのConnectionの二つに対して行います。
TypesにはObject TypesやScalar Typesなど複数の種類があるので、それごとにディレクトリを分割していき、
types.ex
でそれらをまとめ上げる形にします。
今回実装を行うPostはObject Typeで定義できるので、以下のように実装を行います。
defmodule BlogAppWeb.Blogs.Objects.Post do
defmacro __using__(_) do
quote do
object :post do
field :id, non_null(:id)
field :title, non_null(:string)
field :body, non_null(:string)
field :image, :string
field :inserted_at, non_null(:naive_datetime)
end
connection(node_type: :post)
end
end
end
defmodule BlogAppWeb.Blogs.Objects do
alias BlogAppWeb.Blogs.Objects
defmacro __using__(_) do
quote do
use Objects.Post
end
end
end
objects.ex
は実装したObject Typeをまとめ上げる役割をしています。
具体的にPostとPost Connectionの定義を行なっているのはpost.ex
です。
quote
内のはじめのブロックでpost
の定義を行なっています。
また、connection(node_type: :post)
はConnectionの定義です。
これでPost、Post Connectionの実装が完了しました。
最後に実装したObject Typeを以下のようにまとめあげます
defmodule BlogAppWeb.Blogs.Types do
use Absinthe.Schema.Notation
use Absinthe.Relay.Schema.Notation, :modern
use BlogAppWeb.Blogs.Objects
end
これでTypesの実装は完了です。
Schemaの実装
次に実際のクエリのfieldをSchemaで実装します。
今回定義したいのは
- Post
- Post Connection
の二つです。これらの定義は以下のように実装できます。
defmodule BlogAppWeb.Blogs.Schema do
use Absinthe.Schema
use Absinthe.Relay.Schema, :modern
import_types(BlogAppWeb.Blogs.Types)
import_types(Absinthe.Type.Custom)
alias BlogAppWeb.Blogs.Resolvers
alias Absinthe.Relay.Connection
query do
@desc "Get posts connection"
connection field :posts, node_type: :post do
resolve(&Resolvers.Post.posts_connection/2)
end
@desc "Get a post of the blog"
field :post, :post do
arg(:id, non_null(:id))
resolve(&Resolvers.Post.find_post/3)
end
end
end
Connection.from_query
によってSQLクエリが実行されます。このメソッドはGraphQLクエリの引数にあるfirst
やbefore
などのカーソルの指定から自動的にSQLクエリにLIMIT
やOFFSET
を追加してくれ、返り値をConnectionのデータフォーマットに整形してくれます。
これでSchemaの実装は完了です。
Resolversの実装
次にResolversの実装を行います。
今回実装するのはschema.ex
内で呼び出している
posts_connection
とfind_post
の二つです。
これらを実装すると以下のようになります。
defmodule BlogAppWeb.Blogs.Resolvers.Post do
alias Absinthe.Relay.Connection
alias BlogApp.Repo
alias BlogApp.Blogs
def posts_connection(pagination_args, _scope) do
Blogs.list_posts_query()
|> Connection.from_query(&Repo.all/1, pagination_args)
end
def find_post(_parent, %{id: id}, _resolution) do
case Blogs.get_post(id) do
nil ->
{:error, "Post ID #{id} not found"}
post ->
{:ok, post}
end
end
end
これでResolversの実装は完了です。
なお、具体的なロジックはContextにあるので、それらを呼び出す形で実装を行なっています。
APIとして呼び出せるようにする
前の項まででGraphQLの実装自体は完了です。最後にAPIとして呼び出せるようにします。
リクエストへのつなぎ合わせは簡単で,router.ex
に以下のように実装を行うだけです。
…
scope "/api" do
pipe_through :api
forward "/graph", Absinthe.Plug, schema: BlogAppWeb.Blogs.Schema
end
…
これで/api/graph
をエンドポイントとしてGraphQLのリクエストを送ることができます。
おまけ: Connectionのクエリでoffsetを指定できるようにする
最後におまけでConnectionクエリでoffsetを指定できるようにする方法を書いておきます。
GraphQLのConnectionのクエリでは、以下のような引数を渡して取得するデータの指定を行います。
- first
- last
- before
- after
これらはカーソルという概念で用いられるものなのですが、これが単純なページネーションをするだけだと少しまどろっこしいので、offsetという引数も使えるようにします。
参考:カーソルとoffsetの説明
Absinthe.Relay.Connection
にはoffset_to_cursor
というメソッドがあります。これは引数に渡した値をオフセットとしてEnd Cursor
に変換してくれるメソッドです。
これを用いてschema.ex
を以下のように修正します。
defmodule BlogAppWeb.Blogs.Schema do
use Absinthe.Schema
use Absinthe.Relay.Schema, :modern
import_types(BlogAppWeb.Blogs.Types)
import_types(Absinthe.Type.Custom)
alias BlogAppWeb.Blogs.Resolvers
alias Absinthe.Relay.Connection
query do
@desc "Get all posts"
connection field :posts, node_type: :post do
arg(:offset, :integer)
resolve(&Resolvers.Post.posts_connection(convert_offset_to_before(&1), &2))
end
@desc "Get a post of the blog"
field :post, :post do
arg(:id, non_null(:uuid))
resolve(&Resolvers.Post.find_post/3)
end
end
defp convert_offset_to_before(pagination_args) do
case pagination_args do
%{offset: offset} ->
Map.merge(pagination_args, %{before: Connection.offset_to_cursor(offset)})
_ ->
pagination_args
end
end
end
これでoffset
を引数に指定することができるようになります。
最後に
一度骨組みを実装してしまえば柔軟に開発を行うことができるのがGraphQLの強みだと思います。
ただGraphQLは言語ごとにライブラリがあるので、言語によってはドキュメントが少なかったり、未熟だったりするので困ることがあります。
この記事がPhoenixでGraphQLを実装をする際に少しでも参考になれば幸いです。
ちなみに今回サンプルに用いた実装は
https://github.com/getty104/blog_app_ex
から取ってきています。
ここにはアプリケーションのコード全てがあるのでよかったら見てみてください。