ent + gqlgenによる
爆速GraphQLバックエンド開発
2025/01/30
本記事は、NRIエンジニアによって2023年12月22日にQiitaに投稿された記事です。
はじめに
この記事では、バックエンドのコード生成が可能なGoのライブラリentとgqlgenを使用して、爆速&省エネでGraphQLバックエンドの開発を行います。
GraphQLを用いたバックエンドの開発負担を、可能な限り軽減することが目的です。
自分用の備忘録でもあるため、蛇足な説明があるかもしれないです。ご容赦ください。
なぜGoでGraphQLバックエンドを開発するのか
GraphQLは、クライアント側で必要なデータの形式や量を柔軟に指定できるAPI用のクエリ言語です。そのため、REST APIとは異なり、必要最低限のリクエストで必要なデータだけを抽出するすることができます。(オーバーフェッチの排除)
一方、リクエストパターンが増加することで、その分サーバー側の開発の負担が増加するというデメリットがあります。
そこで、サーバー側の開発負担を軽減しようと考えるのが自然かと思います。その際、Goはバックエンド機能に関してコード生成可能なライブラリが多くサポートされているため、相性の良い開発言語と言えます。
今回はGoのライブラリとして、ORMにent(拡張パッケージとしてentgql)、GraphQLサーバにgqlgen、DBにmodernc.org/sqliteを使用しました。
爆速&省エネでやりたいこと
本記事の実施内容の概要を、以下の図で示しました。

実施手順は次のようになります。
①. 手動で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サーバ | 0.17.41 |
|
ORM | 0.12.5 |
|
entの拡張パッケージ | 0.4.5 |
|
DB | 1.28.0 |
1. プロジェクト作成
今回のプロジェクト名はexampleとしました。
mkdir go_study
cd go_study
go mod init example
2. インストール
まず、必要なライブラリをインストールします。
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をつけることでビルド対象外とすることもできます。
3. entによるエンティティの作成
まず、次のコマンドで新規エンティティ(Todo)のスケルトンコードを生成します。
go run -mod=mod entgo.io/ent/cmd/ent new Todo
すると、entディレクトリ下にTodoエンティティに関連するファイルが生成されます。
その中の、ent/schema/todo.goは、Todoエンティティの定義ファイルです。
Todoエンティティの具体的な中身は未定義状態のため、手動で実装していきます。
今回は以下のように定義しました。
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ファイルを新規作成し、以下の内容を記述します。
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)
}
}
公式チュートリアルのままでは、entのスキーマからGraphQLのスキーマを自動生成できません。そこで、entgql.NewExtensionにオプションを付けることで、entのスキーマからGraphQLのスキーマの自動生成が可能になり、より一層の省力化が実現できます。
このとき、entgql.WithConfigPathには、この後登場するgqlgen.ymlのパスを指定します。
また、entgql.WithSchemaPathには、自動生成されるGraphQLのスキーマ定義ファイルの配置したいパスを指定します。ここでは、graph/schema/todo.graphqlsとしたため、graph/schemaディレクトリが必要になります。
mkdir graph/schema
entgqlでは、entgql.WithSchemaPathで指定したディレクトリが存在しない場合、エラーになります。
その場合は、手動で当該ディレクトリを作成してください。
これで、entgqlの準備が整いました。
5. gqlgenによるリゾルバの生成
次に、gqlgenのinitコマンドでgqlgen.ymlとserver.goのボイラープレートを取得します。
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を以下のように編集しました。(一部抜粋)
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ディレクトリ下に配置
となるようにしました。
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を以下のように編集します。
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を実行しましょう。
go generate ./ent
entc.goの実行
ent/schema/todo.goで定義されたentのスキーマ定義から以下の2点が生成されます。
- DBアクセス・操作に必要なGoファイル(CRUD APIコード)
- GraphQLのスキーマ定義ファイル (graph/schema/todo.graphqls)
指定したgraphqlsファイルが既に存在していた場合は、内容が上書きされます。
gqlgen generateの実行
graph/schema/todo.graphqlsで定義されたGraphQLのスキーマ定義から、リゾルバなどのファイルが生成されます。
gqlgenでは、gqlgen.ymlのexecmodelresolverで指定したパスが存在しない場合でも、各項目のファイルはディレクトリごと生成されます。
生成されたリゾルバはスケルトンコードのため、内部処理が未実装となっています。
そこで、リゾルバの内部処理は手動で実装する必要があります。
8. リゾルバの実装
リゾルバでは、entgqlで生成されたDBへのCRUD APIコードを用いて、所望の内部処理(ビジネスロジック)を実装します。
具体的には、resolver.goとtodo.resolver.goを編集します。
resolver.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は以下のように編集します。
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周りの実装を行いました。
まず、modernc.org/sqliteをインストールします。
go get modernc.org/sqlite
次に、ent/sqlite_driver.goというGoファイルを新規作成し、以下の内容を記述します。
これは、entの標準機能でmodernc.org/sqliteのサポートを可能にするドライバーラッパーです。
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を以下のように実装します。
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サーバを起動してみましょう。
go run server.go
http://localhost:8080/にアクセスして、queryを送った際の結果は次のようになります。

想定通りの結果を取得することができました。
最後に
いかがでしたでしょうか。
実装したコード量に比べて、生成されたコード量がかなり多いかと思います。
また、本記事と同様の手順でmutationの機能も実装可能です。
さらに、entが生成したCRUD APIコードとリゾルバのスケルトンコードから、リゾルバの内部処理まで自動生成する、ということもできたら更なる爆速&省エネ開発が実現できますね。これは、爆速&省エネでやりたいことの④を自動化するという意味です。(ビジネスロジックが明らかな場合に限りますが)
本記事を通して、ent+gqlgenによる爆速GraphQLバックエンド開発の威力を実感していただけたら幸いです。