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

Introduction

ここでは、EntをGraphQLに接続する方法を学びます。 GraphQLに詳しくない方は、このチュートリアルの前に、GraphQLの Introduction guideをご覧になることをお勧めします。

コードをCloneする(任意)

このチュートリアルのコードは、github.com/a8m/ent-graphql-exampleで公開されており、(Gitを利用して、)各ステップでタグ付けされています。 基本的なセットアップを飛ばして、初期バージョンのGraphQLサーバーから始めたい場合は、以下のようにリポジトリをクローンして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/

Basic Skeleton

gqlgenは、GoでGraphQLサーバを簡単に生成するためのフレームワークです。 このチュートリアルでは、Entの公式インテグレーションについて確認します。

このチュートリアルは、(Todoリストのスキーマを利用可能にした)前回の続きから始まります。 まずはTodoリスト用のシンプルなGraphQLスキーマを作るところから始めましょう。次に、99designs/gqlgenのパッケージをインストールして、設定をしていきます。 todo.graphqlというファイルを作成し、下記の内容を貼り付けましょう。

# GraphQLのscalar型であるTimeをGoの構造体time.Timeにマッピングする
scalar Time

# 後でEntのenum(Go側の型)にマップするenum型を定義する
# https://graphql.org/learn/schema/#enumeration-types
enum Status {
IN_PROGRESS
COMPLETED
}

# 生成されたEntモデルにマッピングするオブジェクト型を定義する
# 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!]
}

# 後述するMutation用のinput型を定義する
# https://graphql.org/learn/schema/#input-types
input TodoInput {
status: Status! = IN_PROGRESS
priority: Int
text: String!
parent: ID
}

# 新しいtodoを作成するためのMutationを定義する
# https://graphql.org/learn/queries/#mutations
type Mutation {
createTodo(todo: TodoInput!): Todo!
}

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

99designs/gqlgenをインストールする:

go get github.com/99designs/gqlgen

gqlgenパッケージの設定は、カレントディレクトリから自動的に読み込まれるgqlgen.ymlというファイルで行えます。 このファイルを追加しましょう。 各設定ディレクティブの意味は、下記ファイルのコメントに書いてある通りです。

# schemaは、gqlgenにGraphQLスキーマがどこにあるかを示します。
schema:
- todo.graphql

# resolverは、リゾルバの実装がどこにあるかを示します。
resolver:
layout: follow-schema
dir: .

# gqlgenは、これらのgoパッケージ内のスキーマにある任意の型名を検索します。
# マッチした場合はそれを使用し、そうでない場合は生成します。

# autobindは、gqlgenに、指定されたGoパッケージ内のGraphQLスキーマに含まれる任意の型名を検索するように指示します。 一致するものがあれば、それを使用し、そうでなければ新しい型を生成します。
autobind:
- todo/ent

# このセクションでは、GraphQLとGoの型システム間の型マッピングを宣言します。
models:
# IDフィールドをGoの'int'として定義します。
ID:
model:
- github.com/99designs/gqlgen/graphql.IntID
# スキーマで定義されたStatus型をマッピングする
Status:
model:
- todo/ent/todo.Status

これで、gqlgenのコード生成を実行する準備が整いました。 プロジェクトルートで、下記のコマンドを実行しましょう。

go run github.com/99designs/gqlgen

上記のコマンドは、gqlgenコードジェネレーターを実行するものです。正常に終了した場合、プロジェクトのディレクトリ構成は下記のようになります。

➜ 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

EntをGQLに接続する

gqlgenのアセットが生成されたので、Entをgqlgenに接続してサーバーを起動する準備が整いました。 このセクションは5つのステップで構成されていますので、注意して下さい。:)

1. Ent用GraphQL拡張機能のインストール

go get entgo.io/contrib/entgql

2. ent/entc.goという名前の新しい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)
}
opts := []entc.Option{
entc.Extensions(ex),
}
if err := entc.Generate("./schema", &gen.Config{}, opts...); err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}

3. ent/entc.goというファイルを実行するように、 ent/generate.goというファイルを下記のように書き換えます。

package ent

//go:generate go run entc.go

ent/entc.goはbuild tagによってビルドの対象外になっているファイルで、go generateコマンドにより、generate.goを通じて実行されるファイルです。

4. gqlgengo generateで実行するために、(プロジェクトルートに)下記のようなgenerate.goというファイルを新規に作成します。

package todo

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

これで、プロジェクトルートでgo generate ./...を実行すると、Entとgqlgenの両方のコード生成がトリガーされます。

go generate ./...

5. gqlgenでは、生成されたResolverを変更して、追加の依存関係を追加できます。 下記の内容をresolver.goに貼り付けて、ent.Clientを依存関係として追加してみましょう。

package todo

import (
"todo/ent"

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

// Resolverはルートリゾルバです
type Resolver struct{ client *ent.Client }

// NewSchemaは、GraphQLのExecutable schemaを作成します。
func NewSchema(client *ent.Client) graphql.ExecutableSchema {
return NewExecutableSchema(Config{
Resolvers: &Resolver{client},
})
}

サーバを実行する

新しくcmd/todoというディレクトリと下記の内容のmain.goというファイルを作成し、GraphQLサーバを作成します。

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

// サーバが:8081でListenするように設定します。
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)
}
}

下記のコマンドでサーバを起動し、localhost:8081を開きます。

go run ./cmd/todo

インタラクティブなplaygroundが見えているはずです。

tutorial-todo-playground

もし、プレイグラウンドの実行に問題がある場合は、最初のセクションに進み、exampleリポジトリをクローンしてください。

Todoをクエリする

Todoリストをクエリしようとすると、リゾルバのメソッドが未実装なため、エラーが発生します。 QueryリゾルバTodosの内容を変えて、リゾルバを実装してみましょう。

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

そして、このGraphQLクエリを実行すると、空のTodoリストが返ってくるはずです。

query AllTodos {
todos {
id
}
}

# アウトプット: { "data": { "todos": [] } }

Todoを作成する

以前と同じように、GraphQLでTodoを作ろうとすると、リゾルバが未実装のため、エラーが発生します。 MutationリゾルバCreateTodoの内容を変えて、リゾルバを実装してみましょう。

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). // フィールド"priority"が指定された場合、値をセットします。
SetNillableParentID(todo.Parent). // フィールド"parent_id"が指定された場合、値をセットします。
Save(ctx)
}

これで、Todoの作成がうまくいくはずです。

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 } }
# アウトプット: { "data": { "createTodo": { "id": "2", "text": "Create GraphQL Example", "createdAt": "2021-03-10T15:02:18+02:00", "priority": 1, "parent": null } } }

もし、この例がうまくいかない場合は、最初のセクションに行って、exampleのリポジトリをクローンしてください。


次のセクションでは、Relay Node Interfaceを実装する方法を説明します。また、Entがどのようにそれを自動的にサポートするかを学びます。