用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中实现。
这次我们想要定义的是
-
- 帖子
- 帖子连接
以下是两个选项。这些定义可以实现为下面这样。
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查询中,通过传递以下参数来指定要检索的数据。
-
- 首先
-
- 最后
-
- 之前
- 之后
这些概念被用于光标,并且由于只进行简单的分页可能会有些复杂,所以我们还允许使用偏移量参数。参考:光标和偏移量的说明
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 下载的。
这里包含了整个应用程序的所有代码,如果方便的话,请查看一下。
文献引用
-
- 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的信息。