GraphQL 集成
The ent
framework provides an integration with GraphQL through the 99designs/gqlgen library using the extension option (i.e. it can be extended to support other libraries).
快速指南
要在项目中开启 entgql 扩展, 需要用到 entc
(ent codegen) 包。文档在 这里. 按照这3个步骤在你的项目启用它:
- 创建一个新的Go文件,名字为
ent/entc.go
,并粘贴以下内容到文件:
ent/entc.go
// +build ignore
package main
import (
"log"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"entgo.io/contrib/entgql"
)
func main() {
ex, err := entgql.NewExtension()
if err != nil {
log.Fatalf("creating entgql extension: %v", err)
}
if err := entc.Generate("./schema", &gen.Config{}, entc.Extensions(ex)); err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}
- 编辑
ent/generate.go
文件来执行ent/entc.go
文件:
ent/generate.go
package ent
//go:generate go run -mod=mod entc.go
注意 ent/entc.go
会忽略build tag,并通过generate.go
文件执行 go generate
命令. 完整的示例可以在 ent/contrib 仓库 中找到。
- 在你的项目中运行 codegen 命令:
go generate ./...
在运行 codegen 后,以下附加组件将被添加到你的项目。
Node API
A new file named ent/gql_node.go
was created that implements the Relay Node interface.
为了在 GraphQL resolver使用 ent.Noder
新生成的接口, 将 Node
方法添加到查询解析器(query resolver), 阅读 configuration章节如何使用它。
如果您在schema migration中使用 Universal IDs 选项, NodeType 来源于id的值,可以按照下面的方式使用:
func (r *queryResolver) Node(ctx context.Context, id int) (ent.Noder, error) {
return r.client.Noder(ctx, id)
}
然而,如果你想自定义全局唯一标识符,你可以按以下方式生成NodeType:
func (r *queryResolver) Node(ctx context.Context, guid string) (ent.Noder, error) {
typ, id := parseGUID(guid)
return r.client.Noder(ctx, id, ent.WithFixedNodeType(typ))
}
GQL 配置
以下是todo app的配置示例ent/contrib/entgql/todo.
schema:
- todo.graphql
resolver:
# Tell gqlgen to generate resolvers next to the schema file.
layout: follow-schema
dir: .
# gqlgen will search for any type names in the schema in the generated
# ent package. If they match it will use them, otherwise it will new ones.
autobind:
- entgo.io/contrib/entgql/internal/todo/ent
models:
ID:
model:
- github.com/99designs/gqlgen/graphql.IntID
Node:
model:
# ent.Noder is the new interface generated by the Node template.
- entgo.io/contrib/entgql/internal/todo/ent.Noder
分页
分页模板根据 Relay Cursor Connections Spec 添加了分页支持。 更多关于Relay Spec的信息 可以在其 网站 中找到。
连接顺序
The ordering option allows us to apply an ordering on the edges returned from a connection.
Usage Notes
- The generated types will be
autobind
ed to GraphQL types if a naming convention is preserved (see example below). - Ordering can only be defined on ent fields (no edges).
- Ordering fields should normally be indexed to avoid full table DB scan.
- Pagination queries can be sorted by a single field (no order by … then by … semantics).
样例
Let’s go over the steps needed in order to add ordering to an existing GraphQL type. The code example is based on a todo-app that can be found in ent/contrib/entql/todo.
Defining order fields in ent/schema
Ordering can be defined on any comparable field of ent by annotating it with entgql.Annotation
. Note that the given OrderField
name must match its enum value in graphql schema.
func (Todo) Fields() []ent.Field {
return []ent.Field{
field.Time("created_at").
Default(time.Now).
Immutable().
Annotations(
entgql.OrderField("CREATED_AT"),
),
field.Enum("status").
NamedValues(
"InProgress", "IN_PROGRESS",
"Completed", "COMPLETED",
).
Annotations(
entgql.OrderField("STATUS"),
),
field.Int("priority").
Default(0).
Annotations(
entgql.OrderField("PRIORITY"),
),
field.Text("text").
NotEmpty().
Annotations(
entgql.OrderField("TEXT"),
),
}
}
That’s all the schema changes required, make sure to run go generate
to apply them.
Define ordering types in GraphQL schema
Next we need to define the ordering types in graphql schema:
enum OrderDirection {
ASC
DESC
}
enum TodoOrderField {
CREATED_AT
PRIORITY
STATUS
TEXT
}
input TodoOrder {
direction: OrderDirection!
field: TodoOrderField
}
Note that the naming must take the form of <T>OrderField
/ <T>Order
for autobind
ing to the generated ent types. Alternatively @goModel directive can be used for manual type binding.
Adding orderBy argument to the pagination query
type Query {
todos(
after: Cursor
first: Int
before: Cursor
last: Int
orderBy: TodoOrder
): TodoConnection
}
That’s all for the GraphQL schema changes, let’s run gqlgen
code generation.
Update the underlying resolver
Head over to the Todo resolver and update it to pass orderBy
argument to .Paginate()
call:
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),
)
}
Use in GraphQL
query {
todos(first: 3, orderBy: {direction: DESC, field: TEXT}) {
edges {
node {
text
}
}
}
}
Fields Collection
The collection template adds support for automatic GraphQL fields collection for ent-edges using eager-loading. That means, if a query asks for nodes and their edges, entgql will add automatically With
For example, given this GraphQL query:
query {
users(first: 100) {
edges {
node {
photos {
link
}
posts {
content
comments {
content
}
}
}
}
}
}
The client will execute 1 query for getting the users, 1 for getting the photos, and another 2 for getting the posts, and their comments (4 in total). This logic works both for root queries/resolvers and for the node(s) API.
Schema configuration
In order to configure this option to specific edges, use the entgql.Annotation
as follows:
func (Todo) Edges() []ent.Edge {
return []ent.Edge{
edge.To("children", Todo.Type).
Annotations(entgql.Bind()).
From("parent").
// Bind implies the edge name in graphql schema is
// equivalent to the name used in ent schema.
Annotations(entgql.Bind()).
Unique(),
edge.From("owner", User.Type).
Ref("tasks").
// Map edge names as defined in graphql schema.
Annotations(entgql.MapsTo("taskOwner")),
}
}
Usage and Configuration
The GraphQL extension generates also edge-resolvers for the nodes under the gql_edge.go
file as follows:
func (t *Todo) Children(ctx context.Context) ([]*Todo, error) {
result, err := t.Edges.ChildrenOrErr()
if IsNotLoaded(err) {
result, err = t.QueryChildren().All(ctx)
}
return result, err
}
However, if you need to explicitly write these resolvers by hand, you can add the forceResolver option to your GraphQL schema:
type Todo implements Node {
id: ID!
children: [Todo]! @goField(forceResolver: true)
}
Then, you can implement it on your type resolver.
func (r *todoResolver) Children(ctx context.Context, obj *ent.Todo) ([]*ent.Todo, error) {
// Do something here.
return obj.Edges.ChildrenOrErr()
}
Enum Implementation
The enum template implements the MarshalGQL/UnmarshalGQL methods for enums generated by ent.
Transactional Mutations
The entgql.Transactioner
handler executes each GraphQL mutation in a transaction. The injected client for the resolver is a transactional ent.Client. Hence, code that uses ent.Client
won’t need to be changed. In order to use it, follow these steps:
- In the GraphQL server initialization, use the
entgql.Transactioner
handler as follows:
srv := handler.NewDefaultServer(todo.NewSchema(client))
srv.Use(entgql.Transactioner{TxOpener: client})
- Then, in the GraphQL mutations, use the client from context as follows:
func (mutationResolver) CreateTodo(ctx context.Context, todo TodoInput) (*ent.Todo, error) {
client := ent.FromContext(ctx)
return client.Todo.
Create().
SetStatus(todo.Status).
SetNillablePriority(todo.Priority).
SetText(todo.Text).
SetNillableParentID(todo.Parent).
Save(ctx)
}
Examples
The ent/contrib contains several examples at the moment:
- A complete GraphQL server with a simple Todo App with numeric ID field
- The same Todo App in 1, but with UUID type for the ID field
- The same Todo App in 1 and 2, but with a prefixed ULID or
PULID
as the ID field. This example supports the Relay Node API by prefixing IDs with the entity type rather than employing the ID space partitioning in Universal IDs.
Please note that this documentation is under development. All code parts reside in ent/contrib/entgql, and an example of a todo-app can be found in ent/contrib/entgql/todo.