GraphQL 字段集合
在本部分我们继续 GraphQL 示例 来解释 Ent 如何为 GraphQL 模式实现 GraphQL 字段集合,并在解析器中解决 “N+1 问题”。
克隆代码(可选)
本教程代码可以在 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/
问题提出
GraphQL 中的 “N+1 问题” 表示服务器执行非必要数据库查询来获取节点关联关系(如边),但实际上这是可以避免的。 潜在执行的查询次数(N+1)受根查询返回的节点数量、返回节点的关联关系、递归关系等影响。 意味着潜在的查询次数可能会很多(比 N+1 多得多)。
我们用以下查询来解释这个问题:
query {
users(first: 50) {
edges {
node {
photos {
link
}
posts {
content
comments {
content
}
}
}
}
}
}
在上面的查询中,我们想要获取前50个用户,包括他们的照片、文章及文章的评论。
在初级解决方案中(有问题的方案)服务器在一次查询中获取前50个用户,然后为每个用户执行一次查询获取他们的照片(50次查询),
另外50次查询获取他们的文章。假设每个用户有10篇文章。因此对于每篇文章(每个用户的文章)服务器会额外执行500次查询获取其评论。
这意味着我们总共有 1+50+50+500=601 个查询。

Ent 解决方案
Ent 字段集合扩展使用 预加载 为字段集合添加自动化 GraphQL 字段集合 支持。
这意味着如果请求一个节点和他们的边,entgql 会自动添加 With<E> 步骤到根查询中,
作为结果,客户端会向数据库执行恒定数量的查询作为一个结果,并且这是递归工作的。
在上面的 GraphQL 查询中,客户端会执行一个查询获取用户,一个查询获取照片,另外两个查询获取文章及其评论**(总共4个!)**。 这种逻辑适用于根查询/解析器和节点 API。
示例
为达到示例的目的,我们 禁用自动化字段集合,修改 ent.Client 以调试模式运行 Todos 解析器,并重启 GraphQL 服务器:
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().
+ return r.client.Debug().Todo.Query().
Paginate(ctx, after, first, before, last,
ent.WithTodoOrder(orderBy),
)
}
执行 分页教程 中的 GraphQL 查询,并在结果中添加 parent 边:
query {
todos(last: 10, orderBy: {direction: DESC, field: TEXT}) {
edges {
node {
id
text
parent {
id
}
}
cursor
}
}
}
查看处理输出,你会发现服务器向数据库执行了11个查询,一个查询获取最后10个待办事项条目,另外10个查询获取每个条目的父项:
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` ORDER BY `id` ASC LIMIT 11
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
让我们看一下 Ent 如何自动化解决我们的问题:当定义一个 Ent 边时,entgql 自动将其绑定到它在 GraphQL 的使用中,
并为 gql_edge.go 文件下的节点生成边解析器:
func (t *Todo) Children(ctx context.Context) ([]*Todo, error) {
if fc := graphql.GetFieldContext(ctx); fc != nil && fc.Field.Alias != "" {
result, err = t.NamedChildren(graphql.GetFieldContext(ctx).Field.Alias)
} else {
result, err = t.Edges.ChildrenOrErr()
}
if IsNotLoaded(err) {
result, err = t.QueryChildren().All(ctx)
}
return result, err
}
如果我们没有 禁用自动化字段集合 时查看处理输出,我们会发现这一次服务器只向数据库执行了两个查询。 一个获取10个待办事项,第二个获取第一个查询返回待办事项每个条目的父条目。
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority`, `todos`.`todo_parent` FROM `todos` ORDER BY `id` DESC LIMIT 11
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` WHERE `todos`.`id` IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
如果你在本示例的运行中遇到了困难,到 第一部分 并克隆示例代码仓库。
字段映射
entgql.MapsTo 允许你添加 Ent 模式与 GraphQL 模式之间自定义的字段/边映射。
这对于你想在 GraphQL 模式中将字段或边暴露为不同名称时很有用。示例如下:
// One to one mapping.
field.Int("priority").
Annotations(
entgql.OrderField("PRIORITY_ORDER"),
entgql.MapsTo("priorityOrder"),
)
// Multiple GraphQL fields can map to the same Ent field.
field.Int("category_id").
Annotations(
entgql.MapsTo("categoryID", "category_id", "categoryX"),
)
做得很好!通过在 Ent 模式定义中使用自动化字段集合,我们能够在应用程序中极大提升 GraphQL 查询性能。 在下一部分,我们会学习如何创建事务化 GraphQL 突变。