ent + gqlgenによる爆速GraphQLバックエンド開発

# はじめに
この記事では、バックエンドのコード生成が可能なGoのライブラリ`ent`と`gqlgen`を使用して、爆速＆省エネでGraphQLバックエンドの開発を行います。
GraphQLを用いたバックエンドの開発負担を、可能な限り軽減することが目的です。

自分用の備忘録でもあるため、蛇足な説明があるかもしれないです。ご容赦ください。

# なぜGoでGraphQLバックエンドを開発するのか
GraphQLは、クライアント側で必要なデータの形式や量を柔軟に指定できるAPI用のクエリ言語です。そのため、REST APIとは異なり、必要最低限のリクエストで必要なデータだけを抽出するすることができます。（オーバーフェッチの排除）

一方、リクエストパターンが増加することで、その分サーバー側の開発の負担が増加するというデメリットがあります。

そこで、サーバー側の開発負担を軽減しようと考えるのが自然かと思います。その際、Goはバックエンド機能に関してコード生成可能なライブラリが多くサポートされているため、相性の良い開発言語と言えます。

今回はGoのライブラリとして、ORMに`ent`（拡張パッケージとして`entgql`）、GraphQLサーバに`gqlgen`、DBに`modernc.org/sqlite`を使用しました。

# 爆速＆省エネでやりたいこと
本記事の実施内容の概要を、以下の図で示しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989352/400fd2c8-4f38-74f7-67a0-2798dfc3883b.png)


実施手順は次のようになります。
①. 手動で`ent`のスキーマを定義
②. `entgql`でGraphQLスキーマ定義とDB接続で用いるCRUD APIコードを生成
③. `gqlgen`でリゾルバのスケルトンコードを生成
④. 手動でリゾルバの内部処理を実装（CRUD APIコードを使用）

本記事でやりたいことは、GraphQLバックエンドの開発にあたって、実装部分をできるだけ自動生成に置き換えることです。

各ライブラリの簡単な説明は以下になります。
* gqlgen：GraphQLのスキーマ定義からリゾルバの自動生成（スケルトンコード）が可能
* ent：RDBのデータをグラフ型構造にマッピングすることでGraphQLによるDB操作が可能
* entgql：entの拡張パッケージ。GraphQL integrationという機能によって、entのスキーマ定義からGraphQLのスキーマ定義の自動生成が可能

## 動作環境
|言語/ライブラリ等|名称  |バージョン  |
|---|---|---|
|言語  |Go  |1.20  |
|GraphQLサーバ  |[gqlgen](https://gqlgen.com/)  |0.17.41  |
|ORM  |[ent](https://entgo.io/)  |0.12.5  |
|entの拡張パッケージ  |[entgql](https://pkg.go.dev/entgo.io/contrib/entgql)  |0.4.5  |
|DB  |[modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite)  |1.28.0  |

## 1. プロジェクト作成
今回のプロジェクト名はexampleとしました。
```terminal
mkdir go_study
cd go_study
go mod init example
```


## 2. インストール
まず、必要なライブラリをインストールします。
```terminal
go get github.com/99designs/gqlgen
go get entgo.io/ent/cmd/ent
go get entgo.io/contrib/entgql
```
ただし、`modernc.org/sqlite`は後ほどインストールします。
途中で`go mod tidy`を行うため、その時には不要とされ削除されてしまうためです。
以下の記事のように、build tagをつけることでビルド対象外とすることもできます。

https://zenn.dev/kenghaya/articles/2f41736042e054

## 3. entによるエンティティの作成
まず、次のコマンドで新規エンティティ（Todo）のスケルトンコードを生成します。
```terminal
go run -mod=mod entgo.io/ent/cmd/ent new Todo
```

すると、entディレクトリ下にTodoエンティティに関連するファイルが生成されます。

その中の、`ent/schema/todo.go`は、Todoエンティティの定義ファイルです。
Todoエンティティの具体的な中身は未定義状態のため、手動で実装していきます。
今回は以下のように定義しました。
```go: ent/schema/todo.go
package schema

import (
	"entgo.io/contrib/entgql"
	"entgo.io/ent"
	"entgo.io/ent/schema"
	// "entgo.io/ent/schema/edge"
	"entgo.io/ent/schema/field"
)

// Todo holds the schema definition for the Todo entity.
type Todo struct {
	ent.Schema
}

// Fields of the Todo.
func (Todo) Fields() []ent.Field {
	return []ent.Field{
		field.String("name"),
		field.String("email").
			NotEmpty().
			MaxLen(255),
		field.Bool("done").
			Default(false),
	}
}

// Edges of the Todo.
func (Todo) Edges() []ent.Edge {
	return nil
}

func (Todo) Annotations() []schema.Annotation {
	return []schema.Annotation{
		entgql.QueryField(),
		entgql.Mutations(entgql.MutationCreate(), entgql.MutationUpdate()),
	}
}
```
ここで、`Fields()`はTodoテーブルの各フィールドのデータ型や制限などを定義し、`Edges()`はTodoテーブルと他テーブルの関係性を定義します。
また、`Annotations()`では紐づけるテーブルや自動生成するリゾルバを定義できます。

今回は簡単な動作確認が目的なので、`Edges()`の定義は省略します。


## 4. entgqlによるGraphQLのスキーマ定義ファイルの生成
`ent`の拡張パッケージである`entgql`を有効にすることで、`ent`のスキーマ定義とGraphQLのスキーマ定義を紐づけることができます。`entgql`を有効にするには、`entc` (ent codegen) パッケージを使用する必要があります。

まず、`ent/entc.go`というGoファイルを新規作成し、以下の内容を記述します。
```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() {
	entGqlEx, err := entgql.NewExtension(
        entgql.WithConfigPath("../gqlgen.yml"),
		entgql.WithSchemaGenerator(),
		entgql.WithWhereInputs(true),
		entgql.WithSchemaPath("../graph/schema/todo.graphqls"),
	)
    if err != nil {
        log.Fatalf("creating entgql extension: %v", err)
    }
    if err := entc.Generate("./schema", &gen.Config{}, entc.Extensions(entGqlEx)); err != nil {
        log.Fatalf("running ent codegen: %v", err)
    }
}
```
[公式チュートリアル](https://entgo.io/docs/graphql/)のままでは、`ent`のスキーマからGraphQLのスキーマを自動生成できません。そこで、`entgql.NewExtension`にオプションを付けることで、`ent`のスキーマからGraphQLのスキーマの自動生成が可能になり、より一層の省力化が実現できます。

このとき、`entgql.WithConfigPath`には、この後登場する`gqlgen.yml`のパスを指定します。
また、`entgql.WithSchemaPath`には、自動生成されるGraphQLのスキーマ定義ファイルの配置したいパスを指定します。ここでは、`graph/schema/todo.graphqls`としたため、`graph/schema`ディレクトリが必要になります。

```terminal
mkdir graph/schema
```

:::note warn
`entgql`では、`entgql.WithSchemaPath`で指定したディレクトリが存在しない場合、エラーになります。

その場合は、手動で当該ディレクトリを作成してください。
:::

これで、`entgql`の準備が整いました。

## 5. gqlgenによるリゾルバの生成
次に、`gqlgen`のinitコマンドで`gqlgen.yml`と`server.go`のボイラープレートを取得します。
```terminal
go run github.com/99designs/gqlgen init
```
initコマンドでは、graphディレクトリに`generated.go`、`resolver.go`、`schema.graphqls`、`schema.resolvers.go`、`model/models_gen.go`が生成されますが、後ほど`gqlgen.yml`を書き換え、generateコマンドで再生成するため、一旦削除します。

この時、ディレクトリ構成は以下のようになっているかと思います。
```
go_study/
 ├ ent/
 │　└ ...
 ├ graph/
 │　└ schema/
 ├ go.mod
 ├ go.sum
 ├ gqlgen.yml
 └ server.go
```

そして、`gqlgen.yml`を編集し、生成されるファイルの配置したいパスを指定します。
今回は、`gqlgen.yml`を以下のように編集しました。（一部抜粋）
```yml: gqlgen.ymlの一部抜粋
# Where are all the schema files located? globs are supported eg  src/**/*.graphqls
schema:
  - graph/schema/*.graphqls

# Where should the generated server code go?
exec:
  filename: resolver/generated.go
  package: resolver

# Where should any generated models go?
model:
  filename: resolver/model/models_gen.go
  package: model

# Where should the resolver implementations go?
resolver:
  layout: follow-schema
  dir: resolver
  package: resolver
  filename_template: "{name}.resolvers.go"
  # Optional: turn on to not generate template comments above resolvers
  # omit_template_comment: false
```
それぞれの項目では、以下のような内容を指定します。
* `schema`: 読み込むgraphqlsファイルが**配置されているパス**
* `exec`: generated.goファイルを**配置したいパス**とパッケージ名
* `model`: models_gen.goファイルを**配置したいパス**とパッケージ名
* `resolver`: リゾルバの実装ファイルを**配置したいパス**や名前テンプレートなど

ここでは、

* graphqlsファイルはgraphディレクトリ下に配置
* それ以外のGoファイルはresolverディレクトリ下に配置

となるようにしました。

:::note warn
`schema`では`entgql.WithSchemaPath`で指定したパスを含む必要があります。
graphqlsファイルはまだ存在しませんが、entgqlによって生成されるためです。
（後ほど`entgql`→`gqlgen`の順に実行します）

`gqlgen`実行時に、`schema`で指定した先にgraphqlsファイルが存在しない場合、graphqlsファイルが読み込めなかったという直接的なエラーではなく、その依存関係先で生じたエラーが出力されます。
:::
私の場合は、`model`の指定パスが読み込めない、というエラー内容でした。
そのため、実は`schema`のパス設定に問題があるにもかかわらず、`model`のパス設定等に問題があると思い、一度行き詰まってしまいました。

これで、`gqlgen`の準備も整いました。

## 6. go generateの実行
`go generate`コマンドで`entc.go`と`gqlgen`のgenerateコマンドが順次実行されるように、既存の`ent/generate.go`を以下のように編集します。

```go: ent/generate.go
package ent

//go:generate go run -mod=mod entc.go
//go:generate go mod tidy
//go:generate go run -mod=mod github.com/99designs/gqlgen generate

```
これで、GraphQLバックエンドに必要なコードの大部分を自動生成することができます。
ただし、`entc.go`の後に`go mod tidy`の実行を求められるため、事前に記述しました。

では`go generate`を実行しましょう。
```terminal
go generate ./ent
```
### entc.goの実行
`ent/schema/todo.go`で定義された`ent`のスキーマ定義から以下の２点が生成されます。
* DBアクセス・操作に必要なGoファイル（CRUD APIコード）
* GraphQLのスキーマ定義ファイル (`graph/schema/todo.graphqls`)

指定したgraphqlsファイルが既に存在していた場合は、内容が上書きされます。
### gqlgen generateの実行
`graph/schema/todo.graphqls`で定義されたGraphQLのスキーマ定義から、リゾルバなどのファイルが生成されます。

:::note info
`gqlgen`では、`gqlgen.yml`の`exec` `model` `resolver`で指定したパスが存在しない場合でも、各項目のファイルはディレクトリごと生成されます。
:::

生成されたリゾルバはスケルトンコードのため、内部処理が未実装となっています。
そこで、リゾルバの内部処理は手動で実装する必要があります。

## 8. リゾルバの実装
リゾルバでは、`entgql`で生成されたDBへのCRUD APIコードを用いて、所望の内部処理（ビジネスロジック）を実装します。

具体的には、`resolver.go`と`todo.resolver.go`を編集します。

`resolver.go`は以下のように編集します。
```go: resolver/resolver.go
package resolver

// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.

import (
	"example/ent"

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

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

```
`resolver.go`には、最初からResolver構造体が定義されています。このResolver構造体を`type Resolver struct{ client *ent.Client }`とすることで、この構造体は`ent.Client`型の`client`フィールドを持つことになり、データベースからのデータ取得やデータ更新などの操作が可能になります。

さらに、`NewSchema`関数を定義します。ここでは、新たにGraphQLのスキーマを生成し、それを実行可能な状態（`graphql.ExecutableSchema`）にして返しています。`NewSchema`関数は、データベースの操作とGraphQLクエリの解釈を繋ぎ合わせる「橋渡し」の役割を果たします。これによって、クライアントからのGraphQLクエリがデータベースへの具体的な操作として変換され、またその結果がクライアントへと返却される、という一連の流れが可能になります。

`todo.resolver.go`は以下のように編集します。
```go: resolver/todo.resolver.go
package resolver

// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.41

import (
	"context"
	"example/ent"
	"example/resolver/model"
	"fmt"
	"log"
	"strconv"
)

// Node is the resolver for the node field.
func (r *queryResolver) Node(ctx context.Context, id string) (ent.Noder, error) {
	panic(fmt.Errorf("not implemented: Node - node"))
}

// Nodes is the resolver for the nodes field.
func (r *queryResolver) Nodes(ctx context.Context, ids []string) ([]ent.Noder, error) {
	panic(fmt.Errorf("not implemented: Nodes - nodes"))
}

// Todos is the resolver for the todos field.
func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
	entTodos, err := r.client.Todo.Query().All(ctx)
	if err != nil {
		log.Fatalf("failed querying todos: %v", err)
	}
	modelTodos := make([]*model.Todo, len(entTodos))
	for i, entTodo := range entTodos {
		modelTodos[i] = &model.Todo{
			ID:    strconv.Itoa(entTodo.ID),
			Name:  entTodo.Name,
			Email: entTodo.Email,
			Done:  entTodo.Done,
		}
	}
	return modelTodos, nil
}

// Query returns QueryResolver implementation.
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }

type queryResolver struct{ *Resolver }

```
`todo.resolver.go`には、Resolver構造体の各メソッドが定義されています。
特に、`Todos`関数はGraphQLのtodosフィールドのリゾルバです。この関数を、DBから全てのTodoテーブルのデータを取得し、それをGraphQLクエリが要求する形に変換して返す処理をするように実装します。

### リゾルバのファイルが2つに分けられている理由
`todo.resolver.go`内のリゾルバの内部処理は、直接開発者が書き換えても、その後の自動生成で上書きされる恐れがあります。そこで、`resolver.go`内のResolver構造体に依存性注入（DI）を行うことで、自動生成で上書きされる心配なく、サービス層をDIした新しいリゾルバの使用が可能になります。

## 9. DB周辺の実装
次に、DB周辺の実装を行います。
DBには軽量なRDBであるSQLiteを使用しました。

Go製のSQLiteライブラリには、CGOを利用する`go-sqlite3`などがありますが、これらはgccのインストールが必要になります。そのため、今回はpure-Go (CGO-free) で実行可能な`modernc.org/sqlite`を採用しました。

しかし、`ent`は標準で`modernc.org/sqlite`をサポートしていないため、以下の記事を参考にDB周りの実装を行いました。

https://zenn.dev/nobonobo/articles/e9f17d183c19f6

まず、`modernc.org/sqlite`をインストールします。
```terminal
go get modernc.org/sqlite
```
次に、`ent/sqlite_driver.go`というGoファイルを新規作成し、以下の内容を記述します。
これは、`ent`の標準機能で`modernc.org/sqlite`のサポートを可能にするドライバーラッパーです。
```go: ent/sqlite_driver.go
package ent

import (
	"database/sql"
	"database/sql/driver"
	"fmt"

	"modernc.org/sqlite"
)

type sqliteDriver struct {
	*sqlite.Driver
}

func (d sqliteDriver) Open(name string) (driver.Conn, error) {
	conn, err := d.Driver.Open(name)
	if err != nil {
		return conn, err
	}
	c := conn.(interface {
		Exec(stmt string, args []driver.Value) (driver.Result, error)
	})
	if _, err := c.Exec("PRAGMA foreign_keys = on;", nil); err != nil {
		conn.Close()
		return nil, fmt.Errorf("failed to enable enable foreign keys: %w", err)
	}
	return conn, nil
}

func init() {
	sql.Register("sqlite3", sqliteDriver{Driver: &sqlite.Driver{}})
}

```

## 10. サーバのエンドポイントの実装
最後に、今回のエンドポイントである`server.go`を以下のように実装します。
```go: server.go
package main

import (
	"context"
	"example/ent"
	"example/resolver"
	"log"
	"net/http"
	"os"

	"entgo.io/ent/dialect"
	_ "modernc.org/sqlite"

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

const defaultPort = "8080"

func main() {
	// Create an ent.Client with in-memory SQLite database.
	entOptions := []ent.Option{}
	entOptions = append(entOptions, ent.Debug())
	client, err := ent.Open(dialect.SQLite, "file::memory:?cache=shared", entOptions...)
	if err != nil {
		log.Fatalf("failed opening connection to sqlite: %v", err)
	}
	defer client.Close()
	// Run the automatic migration tool to create all schema resources.
	if err := client.Schema.Create(context.Background()); err != nil {
		log.Fatalf("failed creating schema resources: %v", err)
	}

	ctx := context.Background()
	u, err := client.Todo.
		Create().
		SetName("John").
		SetEmail("hogehoge@gmail.com").
		SetDone(false).
		Save(ctx)
	if err != nil {
		log.Fatalf("failed creating user: %v", err)
	}
	log.Println("user was created: ", u)

	port := os.Getenv("PORT")
	if port == "" {
		port = defaultPort
	}

	srv := handler.NewDefaultServer(resolver.NewSchema(client))

	http.Handle("/", playground.Handler("GraphQL playground", "/query"))
	http.Handle("/query", srv)

	log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
	log.Fatal(http.ListenAndServe(":"+port, nil))
}

```
今回はqueryのみ実装しているため、DBテーブルの作成を手動で実装する必要があります。
簡単のため、テーブル作成はエンドポイントである`server.go`内にハードコーディングしています。（大目に見ていただけると幸いです）


## 11. GraphQLサーバの起動
では、GraphQLサーバを起動してみましょう。
```terminal
go run server.go
```

http://localhost:8080/ にアクセスして、queryを送った際の結果は次のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989352/0adf0040-450a-ef52-4f32-1dd4400eca2a.png)
想定通りの結果を取得することができました。

# 最後に
いかがでしたでしょうか。
実装したコード量に比べて、生成されたコード量がかなり多いかと思います。
また、本記事と同様の手順でmutationの機能も実装可能です。

さらに、`ent`が生成したCRUD APIコードとリゾルバのスケルトンコードから、リゾルバの内部処理まで自動生成する、ということもできたら更なる爆速＆省エネ開発が実現できますね。これは、[爆速&省エネでやりたいこと](#爆速省エネでやりたいこと)の④を自動化するという意味です。（ビジネスロジックが明らかな場合に限りますが）

本記事を通して、ent+gqlgenによる爆速GraphQLバックエンド開発の威力を実感していただけたら幸いです。

ご質問やご指摘等ありましたらコメントまでお願いいたします。

# 参考文献
* [Setting Up](https://entgo.io/docs/tutorial-setup/)
* [GraphQL Integration](https://entgo.io/docs/graphql/)
* [ent + gqlgen で怠惰に GraphQL API を作る](https://blog.arthur1.dev/entry/2023/01/04/163643)
* [entとgqlgenで連携させてみた](https://note.com/ryota_0624/n/n2b22a9c4744f)
* [GraphQLをさくっと実装してみた](https://engineerblog.markerise.com/backend/20230630_480/)
* [entを使った省エネバックエンド開発](https://engineering.mercari.com/blog/entry/20211205-44a97f0e1c/)
* [GraphQLサーバーを動かしてみる](https://zenn.dev/hsaki/books/golang-graphql/viewer/tutorial)
* [Go製CGOフリーなSQLiteドライバーでentを使う](https://zenn.dev/nobonobo/articles/e9f17d183c19f6)
* [go mod tidyで消したくないmoduleも消えてしまう。](https://zenn.dev/kenghaya/articles/2f41736042e054)