6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Elixir PhoenixのGraphQLライブラリ - AbsintheでConnectionを使うところまで実装する

Last updated at Posted at 2019-01-02

はじめに

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に対して実装を行なっていきます

lib/blog_app/blogs/blogs.ex
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
lib/blog_app/blogs/post.ex
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.exsdepsに以下のライブラリを追加します。

{:absinthe, "~> 1.4.0"},
{:absinthe_plug, "~> 1.4.0"},
{:poison, "~> 3.1.0"},
{:absinthe_relay, "~> 1.4"},

absintheabsinthe_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で定義できるので、以下のように実装を行います。

lib/blog_app_web/graphql/blogs/types/objects/post.ex
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
lib/blog_app_web/graphql/blogs/types/objects/objects.ex
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を以下のようにまとめあげます

lib/blog_app_web/graphql/blogs/types/types.ex
defmodule BlogAppWeb.Blogs.Types do
  use Absinthe.Schema.Notation
  use Absinthe.Relay.Schema.Notation, :modern
  use BlogAppWeb.Blogs.Objects
end

これでTypesの実装は完了です。

Schemaの実装

次に実際のクエリのfieldをSchemaで実装します。
今回定義したいのは

  1. Post
  2. Post Connection

の二つです。これらの定義は以下のように実装できます。

lib/blog_app_web/graphql/blogs/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 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クエリの引数にあるfirstbeforeなどのカーソルの指定から自動的にSQLクエリにLIMITOFFSETを追加してくれ、返り値をConnectionのデータフォーマットに整形してくれます。

これでSchemaの実装は完了です。

Resolversの実装

次にResolversの実装を行います。
今回実装するのはschema.ex内で呼び出している
posts_connectionfind_postの二つです。
これらを実装すると以下のようになります。

lib/blog_app_web/graphql/blogs/resolvers/post.ex
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に以下のように実装を行うだけです。

lib/blog_app_web/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のクエリでは、以下のような引数を渡して取得するデータの指定を行います。

  1. first
  2. last
  3. before
  4. 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
から取ってきています。
ここにはアプリケーションのコード全てがあるのでよかったら見てみてください。

参考文献

  1. https://hexdocs.pm/absinthe_relay/Absinthe.Relay.html
  2. https://hexdocs.pm/absinthe/plug-phoenix.html
  3. https://hexdocs.pm/absinthe_relay/Absinthe.Relay.Connection.html#cursor_to_offset/1
6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?