Introduction

In this tutorial, we will learn how to connect Ent to GraphQL and set up the various integrations Ent provides, 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.

If you’re not familiar with GraphQL, it’s recommended to go over its introduction guide before going over this tutorial.

Clone the code (optional)

The code for this tutorial is available under github.com/a8m/ent-graphql-example, and tagged (using Git) in each step. If you want to skip the basic setup and start with the initial version of the GraphQL server, you can clone the repository as follows:

  1. git clone git@github.com:a8m/ent-graphql-example.git
  2. cd ent-graphql-example
  3. go run ./cmd/todo

Basic Setup

This tutorial begins where the previous one ended (with a working Todo-list schema). We start by installing the contrib/entgql Ent extension and use it for generating our first schema. Then, install and configure the 99designs/gqlgen framework for building our GraphQL server and explore the official integration Ent provides to it.

Install and configure entgql

  1. Install entgql:
  1. go get entgo.io/contrib/entgql@master
  1. Add the following annotations to the Todo schema to enable Query and Mutation (creation) capabilities:

ent/schema/todo.go

  1. func (Todo) Annotations() []schema.Annotation {
  2. return []schema.Annotation{
  3. entgql.QueryField(),
  4. entgql.Mutations(entgql.MutationCreate()),
  5. }
  6. }
  1. Create a new Go file named ent/entc.go, and paste the following content:

ent/entc.go

  1. //go: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. // Tell Ent to generate a GraphQL schema for
  12. // the Ent schema in a file named ent.graphql.
  13. entgql.WithSchemaGenerator(),
  14. entgql.WithSchemaPath("ent.graphql"),
  15. )
  16. if err != nil {
  17. log.Fatalf("creating entgql extension: %v", err)
  18. }
  19. opts := []entc.Option{
  20. entc.Extensions(ex),
  21. }
  22. if err := entc.Generate("./ent/schema", &gen.Config{}, opts...); err != nil {
  23. log.Fatalf("running ent codegen: %v", err)
  24. }
  25. }

Introduction - 图1note

The ent/entc.go is ignored using a build tag, and it is executed by the go generate command through the generate.go file.

4. Remove the ent/generate.go file and create a new one in the root of the project with the following contents. In next steps, gqlgen commands will be added to this file as well.

generate.go

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

Running schema generation

After installing and configuring entgql, it is time to execute the codegen:

  1. go generate .

You’ll notice a new file was created named ent.graphql:

ent.graphql

  1. directive @goField(forceResolver: Boolean, name: String) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
  2. directive @goModel(model: String, models: [String!]) on OBJECT | INPUT_OBJECT | SCALAR | ENUM | INTERFACE | UNION
  3. """
  4. Define a Relay Cursor type:
  5. https://relay.dev/graphql/connections.htm#sec-Cursor
  6. """
  7. scalar Cursor
  8. """
  9. An object with an ID.
  10. Follows the [Relay Global Object Identification Specification](https://relay.dev/graphql/objectidentification.htm)
  11. """
  12. interface Node @goModel(model: "todo/ent.Noder") {
  13. """The id of the object."""
  14. id: ID!
  15. }
  16. # ...

Install and configure gqlgen

  1. Install 99designs/gqlgen:
  1. go get github.com/99designs/gqlgen
  1. The gqlgen package can be configured using a gqlgen.yml file that is automatically loaded from the current directory. Let’s add this file to the root of the project. Follow the comments in this file to understand what each config directive means:

gqlgen.yml

  1. # schema tells gqlgen where the GraphQL schema is located.
  2. schema:
  3. - ent.graphql
  4. # resolver reports where the resolver implementations go.
  5. resolver:
  6. layout: follow-schema
  7. dir: .
  8. # gqlgen will search for any type names in the schema in these go packages
  9. # if they match it will use them, otherwise it will generate them.
  10. # autobind tells gqngen to search for any type names in the GraphQL schema in the
  11. # provided package. If they match it will use them, otherwise it will generate new.
  12. autobind:
  13. - todo/ent
  14. - todo/ent/todo
  15. # This section declares type mapping between the GraphQL and Go type systems.
  16. models:
  17. # Defines the ID field as Go 'int'.
  18. ID:
  19. model:
  20. - github.com/99designs/gqlgen/graphql.IntID
  21. Node:
  22. model:
  23. - todo/ent.Noder
  1. Edit the ent/entc.go to let Ent know about the gqlgen configuration:
  1. //go: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. // Tell Ent to generate a GraphQL schema for
  12. // the Ent schema in a file named ent.graphql.
  13. entgql.WithSchemaGenerator(),
  14. entgql.WithSchemaPath("ent.graphql"),
  15. entgql.WithConfigPath("gqlgen.yml"),
  16. )
  17. if err != nil {
  18. log.Fatalf("creating entgql extension: %v", err)
  19. }
  20. opts := []entc.Option{
  21. entc.Extensions(ex),
  22. }
  23. if err := entc.Generate("./ent/schema", &gen.Config{}, opts...); err != nil {
  24. log.Fatalf("running ent codegen: %v", err)
  25. }
  26. }
  1. Add the gqlgen generate command to the generate.go file:

generate.go

  1. package todo
  2. //go:generate go run -mod=mod ./ent/entc.go
  3. //go:generate go run -mod=mod github.com/99designs/gqlgen

Now, we’re ready to run go generate to trigger ent and gqlgen code generation. Execute the following command from the root of the project:

  1. go generate .

You may have noticed that some files were generated by gqlgen:

  1. tree -L 1
  2. .
  3. ├── ent/
  4. ├── ent.graphql
  5. ├── ent.resolvers.go
  6. ├── example_test.go
  7. ├── generate.go
  8. ├── generated.go
  9. ├── go.mod
  10. ├── go.sum
  11. ├── gqlgen.yml
  12. └── resolver.go

Basic Server

Before building the GraphQL server we need to set up the main schema Resolver defined in resolver.go. gqlgen allows changing the generated Resolver and adding dependencies to it. Let’s use ent.Client as a dependency by pasting the following in resolver.go:

resolver.go

  1. package todo
  2. import (
  3. "todo/ent"
  4. "github.com/99designs/gqlgen/graphql"
  5. )
  6. // Resolver is the resolver root.
  7. type Resolver struct{ client *ent.Client }
  8. // NewSchema creates a graphql executable schema.
  9. func NewSchema(client *ent.Client) graphql.ExecutableSchema {
  10. return NewExecutableSchema(Config{
  11. Resolvers: &Resolver{client},
  12. })
  13. }

After setting up the main resolver, we create a new directory cmd/todo and a main.go file with the following code to set up a GraphQL server:

cmd/todo/main.go

  1. package main
  2. import (
  3. "context"
  4. "log"
  5. "net/http"
  6. "todo"
  7. "todo/ent"
  8. "todo/ent/migrate"
  9. "entgo.io/ent/dialect"
  10. "github.com/99designs/gqlgen/graphql/handler"
  11. "github.com/99designs/gqlgen/graphql/playground"
  12. _ "github.com/mattn/go-sqlite3"
  13. )
  14. func main() {
  15. // Create ent.Client and run the schema migration.
  16. client, err := ent.Open(dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
  17. if err != nil {
  18. log.Fatal("opening ent client", err)
  19. }
  20. if err := client.Schema.Create(
  21. context.Background(),
  22. migrate.WithGlobalUniqueID(true),
  23. ); err != nil {
  24. log.Fatal("opening ent client", err)
  25. }
  26. // Configure the server and start listening on :8081.
  27. srv := handler.NewDefaultServer(todo.NewSchema(client))
  28. http.Handle("/",
  29. playground.Handler("Todo", "/query"),
  30. )
  31. http.Handle("/query", srv)
  32. log.Println("listening on :8081")
  33. if err := http.ListenAndServe(":8081", nil); err != nil {
  34. log.Fatal("http server terminated", err)
  35. }
  36. }

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

  1. go run ./cmd/todo

You should see the interactive playground:

tutorial-todo-playground

If you are having trouble 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:

ent.resolvers.go

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

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

  • GraphQL
  • Output
  1. query AllTodos {
  2. todos {
  3. id
  4. }
  5. }
  1. {
  2. "data": {
  3. "todos": []
  4. }
  5. }

Mutating Todos

As you can see above, our GraphQL schema returns an empty list of todo items. Let’s create a few todo items, but this time we’ll do it from GraphQL. Luckily, Ent provides auto generated mutations for creating and updating nodes and edges.

  1. We start by extending our GraphQL schema with custom mutations. Let’s create a new file named todo.graphql and add our Mutation type:

todo.graphql

  1. type Mutation {
  2. # The input and the output are types generated by Ent.
  3. createTodo(input: CreateTodoInput!): Todo
  4. }
  1. Add the custom GraphQL schema to gqlgen.yml configuration:

gqlgen.yml

  1. schema:
  2. - ent.graphql
  3. - todo.graphql
  4. # ...
  1. Run code generation:
  1. go generate .

As you can see, gqlgen generated for us a new file named todo.resolvers.go with the createTodo resolver. Let’s connect it to Ent generated code, and ask Ent to handle this mutation:

todo.resolvers.go

  1. func (r *mutationResolver) CreateTodo(ctx context.Context, input ent.CreateTodoInput) (*ent.Todo, error) {
  2. - panic(fmt.Errorf("not implemented: CreateTodo - createTodo"))
  3. + return r.client.Todo.Create().SetInput(input).Save(ctx)
  4. }
  1. Re-run go run ./cmd/todo again and go to the playground:

Demo

At this stage, we are ready to create a todo item and query it:

  • Mutation
  • Mutation Output
  • Query
  • Query Output
  1. mutation CreateTodo {
  2. createTodo(input: {text: "Create GraphQL Example", status: IN_PROGRESS, priority: 1}) {
  3. id
  4. text
  5. createdAt
  6. priority
  7. }
  8. }
  1. {
  2. "data": {
  3. "createTodo": {
  4. "id": "1",
  5. "text": "Create GraphQL Example",
  6. "createdAt": "2022-09-08T15:20:58.696576+03:00",
  7. "priority": 1,
  8. }
  9. }
  10. }
  1. query {
  2. todos {
  3. id
  4. text
  5. status
  6. }
  7. }
  1. {
  2. "data": {
  3. "todos": [
  4. {
  5. "id": "1",
  6. "text": "Create GraphQL Example",
  7. "status": "IN_PROGRESS"
  8. }
  9. ]
  10. }
  11. }

If you’re having trouble 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.