变更输入类型
在本节中,我们将通过 GraphQL 示例继续讲解,说明如何使用 Go 模板扩展 Ent 代码生成器,并为 GraphQL 变更操作生成可直接应用于 Ent 变更的输入类型对象。
克隆代码(可选)
本教程的代码可在 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/
变更类型
Ent 支持生成变更类型。变更类型可作为 GraphQL 变更的输入,由 Ent 进行处理和验证。通过以下配置告知 Ent 我们的 GraphQL Todo 类型支持创建和更新操作:
ent/schema/todo.go
func (Todo) Annotations() []schema.Annotation {
return []schema.Annotation{
entgql.QueryField(),
entgql.Mutations(entgql.MutationCreate(), entgql.MutationUpdate()),
}
}
接着运行代码生成:
go generate .
您会注意到 Ent 为您生成了两种类型:ent.CreateTodoInput 和 ent.UpdateTodoInput。
变更操作
生成变更输入类型后,我们可以将其连接到 GraphQL 变更:
todo.graphql
type Mutation {
createTodo(input: CreateTodoInput!): Todo!
updateTodo(id: ID!, input: UpdateTodoInput!): Todo!
}
运行代码生成将生成实际变更,之后只需将解析器与 Ent 绑定即可:
go generate .
todo.resolvers.go
// CreateTodo 是 createTodo 字段的解析器
func (r *mutationResolver) CreateTodo(ctx context.Context, input ent.CreateTodoInput) (*ent.Todo, error) {
return r.client.Todo.Create().SetInput(input).Save(ctx)
}
// UpdateTodo 是 updateTodo 字段的解析器
func (r *mutationResolver) UpdateTodo(ctx context.Context, id int, input ent.UpdateTodoInput) (*ent.Todo, error) {
return r.client.Todo.UpdateOneID(id).SetInput(input).Save(ctx)
}
测试 CreateTodo 解析器
首先通过两次执行 createTodo 变更来创建两个待办事项。
变更语句
mutation CreateTodo {
createTodo(input: {text: "Create GraphQL Example", status: IN_PROGRESS, priority: 2}) {
id
text
createdAt
priority
parent {
id
}
}
}
输出结果
{
"data": {
"createTodo": {
"id": "1",
"text": "Create GraphQL Example",
"createdAt": "2021-04-19T10:49:52+03:00",
"priority": 2,
"parent": null
}
}
}
变更语句
mutation CreateTodo {
createTodo(input: {text: "Create Tracing Example", status: IN_PROGRESS, priority: 2}) {
id
text
createdAt
priority
parent {
id
}
}
}
输出结果
{
"data": {
"createTodo": {
"id": "2",
"text": "Create Tracing Example",
"createdAt": "2021-04-19T10:50:01+03:00",
"priority": 2,
"parent": null
}
}
}
测试 UpdateTodo 解析器
最后测试 UpdateTodo 解析器。我们将第二个待办事项的 parent 更新为 1。
mutation UpdateTodo {
updateTodo(id: 2, input: {parentID: 1}) {
id
text
createdAt
priority
parent {
id
text
}
}
}
输出结果
{
"data": {
"updateTodo": {
"id": "2",
"text": "Create Tracing Example",
"createdAt": "2021-04-19T10:50:01+03:00",
"priority": 1,
"parent": {
"id": "1",
"text": "Create GraphQL Example"
}
}
}
}
通过变更创建边关系
要在同一变更中创建节点的边关系,可通过边字段扩展 GQL 变更输入:
extended.graphql
extend input CreateTodoInput {
createChildren: [CreateTodoInput!]
}
再次运行代码生成:
go generate .
GQLGen 将为 createChildren 字段生成解析器,您可在解析器中使用:
extended.resolvers.go
// CreateChildren 是 createChildren 字段的解析器
func (r *createTodoInputResolver) CreateChildren(ctx context.Context, obj *ent.CreateTodoInput, data []*ent.CreateTodoInput) error {
panic(fmt.Errorf("not implemented: CreateChildren - createChildren"))
}
现在需要实现创建子节点的逻辑:
extended.resolvers.go
// CreateChildren 是 createChildren 字段的解析器
func (r *createTodoInputResolver) CreateChildren(ctx context.Context, obj *ent.CreateTodoInput, data []*ent.CreateTodoInput) error {
// 注意:需要使用上下文中的 Ent 客户端
// 确保所有子节点在同一事务中创建
// 详见:事务性变更章节
c := ent.FromContext(ctx)
builders := make([]*ent.TodoCreate, len(data))
for i := range data {
builders[i] = c.Todo.Create().SetInput(*data[i])
}
todos, err := c.Todo.CreateBulk(builders...).Save(ctx)
if err != nil {
return err
}
ids := make([]int, len(todos))
for i := range todos {
ids[i] = todos[i].ID
}
obj.ChildIDs = append(obj.ChildIDs, ids...)
return nil
}
将以下代码改为使用事务客户端:
todo.resolvers.go
// CreateTodo 是 createTodo 字段的解析器
func (r *mutationResolver) CreateTodo(ctx context.Context, input ent.CreateTodoInput) (*ent.Todo, error) {
return ent.FromContext(ctx).Todo.Create().SetInput(input).Save(ctx)
}
// UpdateTodo 是 updateTodo 字段的解析器
func (r *mutationResolver) UpdateTodo(ctx context.Context, id int, input ent.UpdateTodoInput) (*ent.Todo, error) {
return ent.FromContext(ctx).Todo.UpdateOneID(id).SetInput(input).Save(ctx)
}
测试包含子节点的变更:
变更语句
mutation {
createTodo(input: {
text: "parent", status:IN_PROGRESS,
createChildren: [
{ text: "children1", status: IN_PROGRESS },
{ text: "children2", status: COMPLETED }
]
}) {
id
text
children {
id
text
status
}
}
}
输出结果
{
"data": {
"createTodo": {
"id": "3",
"text": "parent",
"children": [
{
"id": "1",
"text": "children1",
"status": "IN_PROGRESS"
},
{
"id": "2",
"text": "children2",
"status": "COMPLETED"
}
]
}
}
}
启用调试客户端后,可见子节点在同一事务中创建:
2022/12/14 00:27:41 driver.Tx(7e04b00b-7941-41c5-9aee-41c8c2d85312): started
2022/12/14 00:27:41 Tx(7e04b00b-7941-41c5-9aee-41c8c2d85312).Query: query=INSERT INTO `todos` (`created_at`, `priority`, `status`, `text`) VALUES (?, ?, ?, ?), (?, ?, ?, ?) RETURNING `id` args=[2022-12-14 00:27:41.046344 +0700 +07 m=+5.283557793 0 IN_PROGRESS children1 2022-12-14 00:27:41.046345 +0700 +07 m=+5.283558626 0 COMPLETED children2]
2022/12/14 00:27:41 Tx(7e04b00b-7941-41c5-9aee-41c8c2d85312).Query: query=INSERT INTO `todos` (`text`, `created_at`, `status`, `priority`) VALUES (?, ?, ?, ?) RETURNING `id` args=[parent 2022-12-14 00:27:41.047455 +0700 +07 m=+5.284669251 IN_PROGRESS 0]
2022/12/14 00:27:41 Tx(7e04b00b-7941-41c5-9aee-41c8c2d85312).Exec: query=UPDATE `todos` SET `todo_parent` = ? WHERE `id` IN (?, ?) AND `todo_parent` IS NULL args=[3 1 2]
2022/12/14 00:27:41 Tx(7e04b00b-7941-41c5-9aee-41c8c2d85312).Query: query=SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` WHERE `todo_parent` = ? args=[3]
2022/12/14 00:27:41 Tx(7e04b00b-7941-41c5-9aee-41c8c2d85312): committed