Relay 游标连接(分页)
在本节中,我们将基于 GraphQL 示例继续讲解如何实现 Relay 游标连接规范。如果您不熟悉游标连接接口,请阅读以下摘自 relay.dev 的段落:
在查询中,连接模型提供了切片和分页结果集的标准机制。
在响应中,连接模型提供了提供游标的标准方式,以及告知客户端何时还有更多结果的方法。
以下查询展示了所有这四个要素的示例:
{
user {
id
name
friends(first: 10, after: "opaqueCursor") {
edges {
cursor
node {
id
name
}
}
pageInfo {
hasNextPage
}
}
}
}
克隆代码(可选)
本教程的代码可在 github.com/a8m/ent-graphql-example 获取,并在每个步骤中打上了 Git 标签。如果您想跳过基础设置,直接从 GraphQL 服务器的初始版本开始,可以按如下方式克隆仓库:
git clone git@github.com:a8m/ent-graphql-example.git
cd ent-graphql-example
go run ./cmd/todo/
向 Schema 添加注解
通过使用 entgql.Annotation 注解任何可比较的 Ent 字段,可以定义排序。请注意,给定的 OrderField 名称必须大写,且与 GraphQL schema 中的枚举值匹配。
func (Todo) Fields() []ent.Field {
return []ent.Field{
field.Text("text").
NotEmpty().
Annotations(
entgql.OrderField("TEXT"),
),
field.Time("created_at").
Default(time.Now).
Immutable().
Annotations(
entgql.OrderField("CREATED_AT"),
),
field.Enum("status").
NamedValues(
"InProgress", "IN_PROGRESS",
"Completed", "COMPLETED",
).
Default("IN_PROGRESS").
Annotations(
entgql.OrderField("STATUS"),
),
field.Int("priority").
Default(0).
Annotations(
entgql.OrderField("PRIORITY"),
),
}
}
按多个字段排序
默认情况下,orderBy 参数只接受单个 <T>Order 值。要启用多字段排序,只需将 entgql.MultiOrder() 注解添加到所需的 schema 中。
func (Todo) Annotations() []schema.Annotation {
return []schema.Annotation{
entgql.MultiOrder(),
}
}
通过向 Todo schema 添加此注解,orderBy 参数将从 TodoOrder 更改为 [TodoOrder!]。
按边数量排序
非唯一边可以使用 OrderField 注解进行标注,以便根据特定边类型的数量对节点进行排序。
func (Todo) Edges() []ent.Edge {
return []ent.Edge{
edge.To("children", Todo.Type).
Annotations(
entgql.RelayConnection(),
entgql.OrderField("CHILDREN_COUNT"),
).
From("parent").
Unique(),
}
}
此排序项的命名约定为:UPPER(<边名称>)_COUNT。例如,CHILDREN_COUNT 或 POSTS_COUNT。
按边字段排序
唯一边可以使用 OrderField 注解进行标注,以允许按其关联的边字段对节点进行排序。例如,按作者姓名对帖子排序,或根据父级优先级对待办事项排序。请注意,要按边字段排序,该字段必须在引用类型中使用 OrderField 进行注解。
此排序项的命名约定为:UPPER(<边名称>)_<边字段>。例如,PARENT_PRIORITY。
// Fields 返回待办事项字段。
func (Todo) Fields() []ent.Field {
return []ent.Field{
// ...
field.Int("priority").
Default(0).
Annotations(
entgql.OrderField("PRIORITY"),
),
}
}
// Edges 返回待办事项边。
func (Todo) Edges() []ent.Edge {
return []ent.Edge{
edge.To("children", Todo.Type).
From("parent").
Annotations(
entgql.OrderField("PARENT_PRIORITY"),
).
Unique(),
}
}
此排序项的命名约定为:UPPER(<边名称>)_<边字段>。例如,PARENT_PRIORITY 或 AUTHOR_NAME。
为查询添加分页支持
1. 启用分页的下一步是告诉 Ent Todo 类型是一个 Relay 连接。
func (Todo) Annotations() []schema.Annotation {
return []schema.Annotation{
entgql.RelayConnection(),
entgql.QueryField(),
entgql.Mutations(entgql.MutationCreate()),
}
}
2. 然后,运行 go generate .,您会注意到 ent.resolvers.go 发生了变化。转到 Todos 解析器,更新它以将分页参数传递给 .Paginate():
func (r *queryResolver) Todos(ctx context.Context, after *ent.Cursor, first *int, before *ent.Cursor, last *int, orderBy *ent.TodoOrder) (*ent.TodoConnection, error) {
return r.client.Todo.Query().
Paginate(ctx, after, first, before, last,
ent.WithTodoOrder(orderBy),
)
}
entgql.RelayConnection() 函数表示节点或边应支持分页。因此,返回的结果是 Relay 连接而不是节点列表([T!]! => <T>Connection!)。
在 schema T(位于 ent/schema 中)上设置此注解可启用此节点的分页,因此,Ent 将为此 schema 生成所有 Relay 类型,例如:<T>Edge、<T>Connection 和 PageInfo。例如:
func (Todo) Annotations() []schema.Annotation {
return []schema.Annotation{
entgql.RelayConnection(),
entgql.QueryField(),
}
}
在边上设置此注解表示此边的 GraphQL 字段应支持嵌套分页,且返回类型是 Relay 连接。例如:
func (Todo) Edges() []ent.Edge {
return []ent.Edge{
edge.To("parent", Todo.Type).
Unique().
From("children").
Annotations(entgql.RelayConnection()),
}
}
生成的 GraphQL schema 将是:
-children: [Todo!]!
+children(first: Int, last: Int, after: Cursor, before: Cursor): TodoConnection!
分页使用
现在,我们已准备好测试新的 GraphQL 解析器。首先通过多次运行以下查询(修改变量是可选的)来创建几个待办事项:
mutation CreateTodo($input: CreateTodoInput!) {
createTodo(input: $input) {
id
text
createdAt
priority
parent {
id
}
}
}
# 查询变量:{ "input": { "text": "Create GraphQL Example", "status": "IN_PROGRESS", "priority": 1 } }
# 输出:{ "data": { "createTodo": { "id": "2", "text": "Create GraphQL Example", "createdAt": "2021-03-10T15:02:18+02:00", "priority": 1, "parent": null } } }
然后,我们可以使用分页 API 查询待办事项列表:
query {
todos(first: 3, orderBy: {direction: DESC, field: TEXT}) {
edges {
node {
id
text
}
cursor
}
}
}
# 输出:{ "data": { "todos": { "edges": [ { "node": { "id": "16", "text": "Create GraphQL Example" }, "cursor": "gqFpEKF2tkNyZWF0ZSBHcmFwaFFMIEV4YW1wbGU" }, { "node": { "id": "15", "text": "Create GraphQL Example" }, "cursor": "gqFpD6F2tkNyZWF0ZSBHcmFwaFFMIEV4YW1wbGU" }, { "node": { "id": "14", "text": "Create GraphQL Example" }, "cursor": "gqFpDqF2tkNyZWF0ZSBHcmFwaFFMIEV4YW1wbGU" } ] } } }
我们还可以使用上面查询中获得的游标来获取其后所有项目。
query {
todos(first: 3, after:"gqFpEKF2tkNyZWF0ZSBHcmFwaFFMIEV4YW1wbGU", orderBy: {direction: DESC, field: TEXT}) {
edges {
node {
id
text
}
cursor
}
}
}
# 输出:{ "data": { "todos": { "edges": [ { "node": { "id": "15", "text": "Create GraphQL Example" }, "cursor": "gqFpD6F2tkNyZWF0ZSBHcmFwaFFMIEV4YW1wbGU" }, { "node": { "id": "14", "text": "Create GraphQL Example" }, "cursor": "gqFpDqF2tkNyZWF0ZSBHcmFwaFFMIEV4YW1wbGU" }, { "node": { "id": "13", "text": "Create GraphQL Example" }, "cursor": "gqFpDqF2tkNyZWF0ZSBHcmFwaFFMIEV4YW1wbGU" } ] } } }
太好了!通过一些简单的更改,我们的应用程序现在支持分页。请继续阅读下一节,我们将解释如何实现 GraphQL 字段集合,并了解 Ent 如何解决 GraphQL 解析器中的 “N+1 问题”。