跳到主要内容

引言

在这节中,我们将学习如何将 Ent 连接到 GraphQL。 如果你对 GraphQL 并不熟悉,建议在阅读这篇教程之前先浏览其介绍指南

克隆代码(可选)

本教程的代码可在 github.com/a8m/ent-graphql-example 获取,每一步过程都打有标签(使用 Git)。 如果你想跳过基础的搭建步骤而从 GraphQL 服务端的初始版本开始,那么你可以使用如下命令克隆仓库并 checkout v0.1.0

git clone git@github.com:a8m/ent-graphql-example.git
cd ent-graphql-example
git checkout v0.1.0
go run ./cmd/todo/

基础骨架

gqlgen is a framework for easily generating GraphQL servers in Go. In this tutorial, we will review Ent's official integration with it.

This tutorial begins where the previous one ended (with a working Todo-list schema). We start by creating a simple GraphQL schema for our todo list, then install the 99designs/gqlgen package and configure it. Let's create a file named todo.graphql and paste the following:

# Maps a Time GraphQL scalar to a Go time.Time struct.
scalar Time

# Define an enumeration type and map it later to Ent enum (Go type).
# https://graphql.org/learn/schema/#enumeration-types
enum Status {
IN_PROGRESS
COMPLETED
}

# Define an object type and map it later to the generated Ent model.
# https://graphql.org/learn/schema/#object-types-and-fields
type Todo {
id: ID!
createdAt: Time
status: Status!
priority: Int!
text: String!
parent: Todo
children: [Todo!]
}

# Define an input type for the mutation below.
# https://graphql.org/learn/schema/#input-types
input TodoInput {
status: Status! = IN_PROGRESS
priority: Int
text: String!
parent: ID
}

# Define a mutation for creating todos.
# https://graphql.org/learn/queries/#mutations
type Mutation {
createTodo(todo: TodoInput!): Todo!
}

# Define a query for getting all todos.
type Query {
todos: [Todo!]
}

Install 99designs/gqlgen:

go get github.com/99designs/gqlgen

The gqlgen package can be configured using a gqlgen.yml file that it automatically loads from the current directory. Let's add this file. Follow the comments in this file to understand what each config directive means:

# schema tells gqlgen where the GraphQL schema is located.
schema:
- todo.graphql

# resolver reports where the resolver implementations go.
resolver:
layout: follow-schema
dir: .

# gqlgen will search for any type names in the schema in these go packages
# if they match it will use them, otherwise it will generate them.

# autobind tells gqlgen to search for any type names in the GraphQL schema in the
# provided Go package. If they match it will use them, otherwise it will generate new ones.
autobind:
- todo/ent

# This section declares type mapping between the GraphQL and Go type systems.
models:
# Defines the ID field as Go 'int'.
ID:
model:
- github.com/99designs/gqlgen/graphql.IntID
# Map the Status type that was defined in the schema
Status:
model:
- todo/ent/todo.Status

Now, we're ready to run gqlgen code generation. Execute this command from the root of the project:

go run github.com/99designs/gqlgen

The command above will execute the gqlgen code-generator, and if that finished successfully, your project directory should look like this:

➜ tree -L 1   
.
├── ent
├── example_test.go
├── generated.go
├── go.mod
├── go.sum
├── gqlgen.yml
├── models_gen.go
├── resolver.go
├── todo.graphql
└── todo.resolvers.go

1 directories, 9 files

Connect Ent to GQL

After the gqlgen assets were generated, we're ready to connect Ent to gqlgen and start running our server. This section contains 5 steps, follow them carefully :).

1. Install the GraphQL extension for Ent

go get entgo.io/contrib/entgql

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

// +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)
}
opts := []entc.Option{
entc.Extensions(ex),
}
if err := entc.Generate("./schema", &gen.Config{}, opts...); err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}

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

package ent

//go:generate go run 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.

4. In order to execute gqlgen through go generate, we create a new generate.go file (in the root of the project) with the following:

package todo

//go:generate go run github.com/99designs/gqlgen

Now, running go generate ./... from the root of the project, triggers both Ent and gqlgen code generation.

go generate ./...

5. gqlgen allows changing the generated Resolver and add additional dependencies to it. Let's add the ent.Client as a dependency by pasting the following in resolver.go:

package todo

import (
"todo/ent"

"github.com/99designs/gqlgen/graphql"
)

// Resolver is the resolver root.
type Resolver struct{ client *ent.Client }

// NewSchema creates a graphql executable schema.
func NewSchema(client *ent.Client) graphql.ExecutableSchema {
return NewExecutableSchema(Config{
Resolvers: &Resolver{client},
})
}

Run the server

We create a new directory cmd/todo and a main.go file with the following code to create the GraphQL server:

package main

import (
"context"
"log"
"net/http"

"todo"
"todo/ent"
"todo/ent/migrate"

"entgo.io/ent/dialect"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"

_ "github.com/mattn/go-sqlite3"
)

func main() {
// Create ent.Client and run the schema migration.
client, err := ent.Open(dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatal("opening ent client", err)
}
if err := client.Schema.Create(
context.Background(),
migrate.WithGlobalUniqueID(true),
); err != nil {
log.Fatal("opening ent client", err)
}

// Configure the server and start listening on :8081.
srv := handler.NewDefaultServer(todo.NewSchema(client))
http.Handle("/",
playground.Handler("Todo", "/query"),
)
http.Handle("/query", srv)
log.Println("listening on :8081")
if err := http.ListenAndServe(":8081", nil); err != nil {
log.Fatal("http server terminated", err)
}
}

Run the server using the command below, and open localhost:8081:

go run ./cmd/todo

You should see the interactive playground:

tutorial-todo-playground

If you're having troubles with getting the playground to run, go to first section and clone the example repository.

Query Todos

If we try to query our todo list, we'll get an error as the resolver method is not yet implemented. Let's implement the resolver by replacing the Todos implementation in the query resolver:

func (r *queryResolver) Todos(ctx context.Context) ([]*ent.Todo, error) {
- panic(fmt.Errorf("not implemented"))
+ return r.client.Todo.Query().All(ctx)
}

Then, running this GraphQL query should return an empty todo list:

query AllTodos {
todos {
id
}
}

# Output: { "data": { "todos": [] } }

Create a Todo

Same as before, if we try to create a todo item in GraphQL, we'll get an error as the resolver is not yet implemented. Let's implement the resolver by changing the CreateTodo implementation in the mutation resolver:

func (r *mutationResolver) CreateTodo(ctx context.Context, todo TodoInput) (*ent.Todo, error) {
return r.client.Todo.Create().
SetText(todo.Text).
SetStatus(todo.Status).
SetNillablePriority(todo.Priority). // Set the "priority" field if provided.
SetNillableParentID(todo.Parent). // Set the "parent_id" field if provided.
Save(ctx)
}

Now, creating a todo item should work:

mutation CreateTodo($todo: TodoInput!) {
createTodo(todo: $todo) {
id
text
createdAt
priority
parent {
id
}
}
}

# Query Variables: { "todo": { "text": "Create GraphQL Example", "status": "IN_PROGRESS", "priority": 1 } }
# Output: { "data": { "createTodo": { "id": "2", "text": "Create GraphQL Example", "createdAt": "2021-03-10T15:02:18+02:00", "priority": 1, "parent": null } } }

If you're having troubles with getting this example to work, go to first section and clone the example repository.


Please continue to the next section where we explain how to implement the Relay Node Interface and learn how Ent automatically supports this.