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/
向模式添加注解
通过 entgql.Annotation 注解,可以在任何可比较的 Ent 字段上定义排序。
需注意 OrderField 名称必须大写且匹配其在 GraphQL 模式中的枚举值。
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() 注解添加到期望的模式中。
func (Todo) Annotations() []schema.Annotation {
return []schema.Annotation{
entgql.MultiOrder(),
}
}
通过将此注解添加到 Todo 模式,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(<edge-name>)_COUNT。例如 CHILDREN_COUNT 或 POSTS_COUNT。
根据边字段排序
唯一的边可以添加 OrderField 注解来根据关联的边字段来对节点进行排序。
例如 根据作者对文章排序 、 根据父项优先级对待办事项排序。
需注意若需根据边字段排序,则字段必须在引用类型中由 OrderField 注解。
排序条目的命名惯例是 UPPER(<edge-name>)_<edge-field>。例如 PARENT_PRIORITY。
// Fields returns todo fields.
func (Todo) Fields() []ent.Field {
return []ent.Field{
// ...
field.Int("priority").
Default(0).
Annotations(
entgql.OrderField("PRIORITY"),
),
}
}
// Edges returns todo edges.
func (Todo) Edges() []ent.Edge {
return []ent.Edge{
edge.To("children", Todo.Type).
From("parent").
Annotations(
entgql.OrderField("PARENT_PRIORITY"),
).
Unique(),
}
}
排序条目的命名惯例是 UPPER(<edge-name>)_<edge-field>。例如 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!)。
在模式 T(位于 ent/schema 中) 上设置此注解为节点启用分页功能,因此 Ent 会为此模式生成全部的 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 模式如下:
-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
}
}
}
# Query Variables: { "input": { "text": "Create GraphQL Example", "status": "IN_PROGRESS", "priority": 1 } }
# Output: { "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
}
}
}
# Output: { "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
}
}
}
# Output: { "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": "gqFpDaF2tkNyZWF0ZSBHcmFwaFFMIEV4YW1wbGU" } ] } } }
很好!通过一些简单修改,我们的应用程序现在已经支持分页功能。 请继续下一部分内容,我们会解释如何实现 GraphQL 字段集合,并学习 Ent 如何在 GraphQL 解析器中解决 "N+1 问题"。