メインコンテンツへスキップする

GraphQLインテグレーション

The Ent framework supports GraphQL using the 99designs/gqlgen library and provides various integrations, such as:

  1. Generating a GraphQL schema for nodes and edges defined in an Ent schema.
  2. Auto-generated Query and Mutation resolvers and provide seamless integration with the Relay framework.
  3. Filtering, pagination (including nested) and compliant support with the Relay Cursor Connections Spec.
  4. Efficient field collection to overcome the N+1 problem without requiring data loaders.
  5. Transactional mutations to ensure consistency in case of failures.

Check out the website's GraphQL tutorial for more information.

クイックスタート

In order to enable the entgql extension to your project, you need to use the entc (ent codegen) package as described here. Follow these 3 steps to enable it to your project:

1. Create a new Go file named ent/entc.go, and paste the following content:

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)
}
}

2. Edit the ent/generate.go file to execute the ent/entc.go file:

ent/generate.go
package ent

//go:generate go run -mod=mod entc.go

Note that ent/entc.go is ignored using a build tag, and it's executed by the go generate command through the generate.go file. The full example can be found in the ent/contrib repository.

3. Run codegen for your ent project:

go generate ./...

After running codegen, the following add-ons will be added to your project.

Node API

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

In order to use the new generated ent.Noder interface in the GraphQL resolver, add the Node method to the query resolver, and look at the configuration section to understand how to use it.

If you are using the Universal IDs option in the schema migration, the NodeType is derived from the id value and can be used as follows:

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

However, if you use a custom format for the global unique identifiers, you can control the NodeType as follows:

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設定

Here's a configuration example for a todo app as exists in ent/contrib/entgql/todo.

schema:
- todo.graphql

resolver:
# gqlgenに、スキーマファイルの隣にリゾルバを生成するように指示する
layout: follow-schema
dir: .

# gqlgenは生成されたentパッケージ内のスキーマに含まれる型名を
# 検索する。 一致すればそれを使い、そうでなければ新しいものを使う
autobind:
- entgo.io/contrib/entgql/internal/todo/ent

models:
ID:
model:
- github.com/99designs/gqlgen/graphql.IntID
Node:
model:
# ent.Noderは、Nodeテンプレートが生成する新しいインターフェースである
- entgo.io/contrib/entgql/internal/todo/ent.Noder

ページネーション

The pagination template adds a pagination support according to the Relay Cursor Connections Spec. More info about the Relay Spec can be found in its website.

コネクションの順序指定

The ordering option allows us to apply an ordering on the edges returned from a connection.

使用上の注意

  • 生成された型は、命名規則が守られていれば、GraphQLの型にautobindされます (以下の例を参照)。
  • 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.

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.

GraphQLスキーマで順序タイプを定義する

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 autobinding to the generated ent types. Alternatively @goModel directive can be used for manual type binding.

ページネーションクエリに orderBy 引数を追加する

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.

基礎となるリゾルバを更新する

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),
)
}

GraphQLで使用する

query {
todos(first: 3, orderBy: {direction: DESC, field: TEXT}) {
edges {
node {
text
}
}
}
}

フィールドのコレクション

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:

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.

スキーマの設定

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").
// バインドは,GraphQLスキーマのエッジ名がentスキーマで
// 使われている名前と同等であることを意味します.
Annotations(entgql.Bind()).
Unique(),
edge.From("owner", User.Type).
Ref("tasks").
// GraphQLスキーマで定義されたエッジ名をマッピングします。
Annotations(entgql.MapsTo("taskOwner")),
}
}

使い方と設定

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の実装

The enum template implements the MarshalGQL/UnmarshalGQL methods for enums generated by ent.

トランザクション・ミューテーション

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:

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

2. 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)
}

使用例

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.