用Elixir Phoenix的GraphQL库 – Absinthe实现到使用Connection

首先

在Elixir的Web框架Phoenix中,我们可以使用名为Absinthe的库来轻松定义和实现GraphQL的Schema和Type等。然而,在实现GraphQL的分页功能时,关于Connection的实现方法日语文档并不多,因此我打算总结一下。既然如此,我将一并介绍Absinthe的使用(不包括Mutation的实现)。

顺便提一下,本文不会对GraphQL进行解释。如果想了解GraphQL的含义,请参考以下网站等资料。 GraphQL介绍。

环境

Elixir 1.7.3 和 Phoenix 1.4.0

我們將根據以下的背景條件進行實施。

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是必要的库,用于定义GraphQL Connection。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文件中將它們整合在一起。

由于这次的实施是可通过对象类型(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.ex中的Post和Post Connection。
在引用内的第一个块中定义了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中实现。
这次我们想要定义的是

    1. 帖子

 

    帖子连接

以下是两个选项。这些定义可以实现为下面这样。

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等游标指示自动添加LIMIT和OFFSET到SQL查询中,并将返回值格式化为Connection的数据格式。

现在Schema的实现已经完成了。

解析器的实现

接下来,我们将进行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查詢中指定偏移量的選項。

在GraphQL的Connection查询中,通过传递以下参数来指定要检索的数据。

    1. 首先

 

    1. 最后

 

    1. 之前

 

    之后

这些概念被用于光标,并且由于只进行简单的分页可能会有些复杂,所以我们还允许使用偏移量参数。参考:光标和偏移量的说明

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

现在你可以通过传递参数来指定偏移量。

最后的

我认为GraphQL的优势在于一旦实现了框架,就可以灵活地进行开发。
然而,由于GraphQL有针对不同语言的库,因此有时可能会遇到一些问题,例如文档较少或不成熟。
如果本文对使用Phoenix实现GraphQL有所帮助,我将非常高兴。

顺便提一下,我们这次使用的实现是从https://github.com/getty104/blog_app_ex 下载的。
这里包含了整个应用程序的所有代码,如果方便的话,请查看一下。

文献引用

    1. https://hexdocs.pm/absinthe_relay/Absinthe.Relay.html 可以在这个链接中找到有关Absinthe.Relay的文档。

https://hexdocs.pm/absinthe/plug-phoenix.html 可以在这个链接中找到关于Absinthe在Phoenix中使用的文档。

https://hexdocs.pm/absinthe_relay/Absinthe.Relay.Connection.html#cursor_to_offset/1 可以在这个链接中找到有关Absinthe.Relay.Connection的cursor_to_offset/1的信息。

广告
将在 10 秒后关闭
bannerAds