GraphQL 集成

ent 框架用 外部模板 方式通过 99designs/gqlgen 库提供了与 GraphQL 的集成(换句话说 通过扩展可以支持其他GraphQL库).

快速指南

要在项目中开启 entgql 扩展, 需要用到 entc (ent codegen) 包。文档在 这里. 按照这3个步骤在你的项目启用它:

1. 创建一个新的Go文件,名字为 ent/entc.go,并粘贴以下内容到文件:

  1. // +build ignore
  2. package main
  3. import (
  4. "log"
  5. "entgo.io/ent/entc"
  6. "entgo.io/ent/entc/gen"
  7. "entgo.io/contrib/entgql"
  8. )
  9. func main() {
  10. ex, err := entgql.NewExtension()
  11. if err != nil {
  12. log.Fatalf("creating entgql extension: %v", err)
  13. }
  14. if err := entc.Generate("./schema", &gen.Config{}, entc.Extensions(ex)); err != nil {
  15. log.Fatalf("running ent codegen: %v", err)
  16. }
  17. }

2. 编辑 ent/generate.go 文件来执行 ent/entc.go 文件:

  1. package ent
  2. //go:generate go run -mod=mod entc.go

注意 ent/entc.go 会忽略build tag,并通过generate.go 文件执行 go generate 命令. 完整的示例可以在 ent/contrib 仓库 中找到。

3. 在你的项目中运行 codegen 命令:

  1. go generate ./...

在运行 codegen 后,以下附加组件将被添加到你的项目。

Node API

A new file named ent/node.go was created that implements the Relay Node interface.

为了在 GraphQL resolver使用 ent.Noder新生成的接口, 将 Node 方法添加到查询解析器(query resolver), 阅读 configuration章节如何使用它。

如果您在schema migration中使用 Universal IDs 选项, NodeType 来源于id的值,可以按照下面的方式使用:

  1. func (r *queryResolver) Node(ctx context.Context, id int) (ent.Noder, error) {
  2. return r.client.Noder(ctx, id)
  3. }

然而,如果你想自定义全局唯一标识符,你可以按以下方式生成NodeType:

  1. func (r *queryResolver) Node(ctx context.Context, guid string) (ent.Noder, error) {
  2. typ, id := parseGUID(guid)
  3. return r.client.Noder(ctx, id, ent.WithFixedNodeType(typ))
  4. }

GQL 配置

以下是todo app的配置示例ent/contrib/entgql/todo.

  1. schema:
  2. - todo.graphql
  3. resolver:
  4. # Tell gqlgen to generate resolvers next to the schema file.
  5. layout: follow-schema
  6. dir: .
  7. # gqlgen will search for any type names in the schema in the generated
  8. # ent package. If they match it will use them, otherwise it will new ones.
  9. autobind:
  10. - entgo.io/contrib/entgql/internal/todo/ent
  11. models:
  12. ID:
  13. model:
  14. - github.com/99designs/gqlgen/graphql.IntID
  15. Node:
  16. model:
  17. # ent.Noder is the new interface generated by the Node template.
  18. - 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 autobinded 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.

  1. func (Todo) Fields() []ent.Field {
  2. return []ent.Field{
  3. field.Time("created_at").
  4. Default(time.Now).
  5. Immutable().
  6. Annotations(
  7. entgql.OrderField("CREATED_AT"),
  8. ),
  9. field.Enum("status").
  10. NamedValues(
  11. "InProgress", "IN_PROGRESS",
  12. "Completed", "COMPLETED",
  13. ).
  14. Annotations(
  15. entgql.OrderField("STATUS"),
  16. ),
  17. field.Int("priority").
  18. Default(0).
  19. Annotations(
  20. entgql.OrderField("PRIORITY"),
  21. ),
  22. field.Text("text").
  23. NotEmpty().
  24. Annotations(
  25. entgql.OrderField("TEXT"),
  26. ),
  27. }
  28. }

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:

  1. enum OrderDirection {
  2. ASC
  3. DESC
  4. }
  5. enum TodoOrderField {
  6. CREATED_AT
  7. PRIORITY
  8. STATUS
  9. TEXT
  10. }
  11. input TodoOrder {
  12. direction: OrderDirection!
  13. field: TodoOrderField
  14. }

Note that the naming must take the form of <T>OrderField / <T>Order for autobinding to the generated ent types. Alternatively @goModel directive can be used for manual type binding.

Adding orderBy argument to the pagination query

  1. type Query {
  2. todos(
  3. after: Cursor
  4. first: Int
  5. before: Cursor
  6. last: Int
  7. orderBy: TodoOrder
  8. ): TodoConnection
  9. }

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:

  1. func (r *queryResolver) Todos(ctx context.Context, after *ent.Cursor, first *int, before *ent.Cursor, last *int, orderBy *ent.TodoOrder) (*ent.TodoConnection, error) {
  2. return r.client.Todo.Query().
  3. Paginate(ctx, after, first, before, last,
  4. ent.WithTodoOrder(orderBy),
  5. )
  6. }

Use in GraphQL

  1. query {
  2. todos(first: 3, orderBy: {direction: DESC, field: TEXT}) {
  3. edges {
  4. node {
  5. text
  6. }
  7. }
  8. }
  9. }

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<E> steps to the root query, and as a result, the client will execute constant number of queries to the database - and it works recursively.

For example, given this GraphQL query:

  1. query {
  2. users(first: 100) {
  3. edges {
  4. node {
  5. photos {
  6. link
  7. }
  8. posts {
  9. content
  10. comments {
  11. content
  12. }
  13. }
  14. }
  15. }
  16. }
  17. }

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:

  1. func (Todo) Edges() []ent.Edge {
  2. return []ent.Edge{
  3. edge.To("children", Todo.Type).
  4. Annotations(entgql.Bind()).
  5. From("parent").
  6. // Bind implies the edge name in graphql schema is
  7. // equivalent to the name used in ent schema.
  8. Annotations(entgql.Bind()).
  9. Unique(),
  10. edge.From("owner", User.Type).
  11. Ref("tasks").
  12. // Map edge names as defined in graphql schema.
  13. Annotations(entgql.MapsTo("taskOwner")),
  14. }
  15. }

Usage and Configuration

The GraphQL extension generates also edge-resolvers for the nodes under the edge.go file as follows:

  1. func (t *Todo) Children(ctx context.Context) ([]*Todo, error) {
  2. result, err := t.Edges.ChildrenOrErr()
  3. if IsNotLoaded(err) {
  4. result, err = t.QueryChildren().All(ctx)
  5. }
  6. return result, err
  7. }

However, if you need to explicitly write these resolvers by hand, you can add the forceResolver option to your GraphQL schema:

  1. type Todo implements Node {
  2. id: ID!
  3. children: [Todo]! @goField(forceResolver: true)
  4. }

Then, you can implement it on your type resolver.

  1. func (r *todoResolver) Children(ctx context.Context, obj *ent.Todo) ([]*ent.Todo, error) {
  2. // Do something here.
  3. return obj.Edges.ChildrenOrErr()
  4. }

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:

1. In the GraphQL server initialization, use the entgql.Transactioner handler as follows:

  1. srv := handler.NewDefaultServer(todo.NewSchema(client))
  2. srv.Use(entgql.Transactioner{TxOpener: client})

2. Then, in the GraphQL mutations, use the client from context as follows:

  1. func (mutationResolver) CreateTodo(ctx context.Context, todo TodoInput) (*ent.Todo, error) {
  2. client := ent.FromContext(ctx)
  3. return client.Todo.
  4. Create().
  5. SetStatus(todo.Status).
  6. SetNillablePriority(todo.Priority).
  7. SetText(todo.Text).
  8. SetNillableParentID(todo.Parent).
  9. Save(ctx)
  10. }

Examples

The ent/contrib contains several examples at the moment:

  1. A complete GraphQL server with a simple Todo App with numeric ID field
  2. The same Todo App in 1, but with UUID type for the ID field
  3. 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.