0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Goで学ぶGraphQLサーバーサイド(2)ーGraphQLサーバーを動かしてみる

Posted at

こんにちは。

Part 2は「GraphQLサーバーを動かしてみる」についてです。

この章について

何はともあれ早速GraphQLのサーバーサイドコードを実際に見てみましょう。

今回はgqlgenというGo用GraphQLサーバーサイドライブラリを使用します。
gqlgenコマンドには、サンプルアプリとしてTODO管理APIのコードを生成させる機能があるため、それを生成させたのちに実際に動かすところまでやっていきたいと思います。

gqlgenコマンド

gqlgenコマンドは、GraphQLのスキーマ情報からGoで書かれたサーバーサイドコードを自動生成させるコマンドです。

インストール

gqlgenコマンドはGo製のコマンドなので、go installコマンドを使ってインストールします。

$ go install github.com/99designs/gqlgen@latest
$ gqlgen version
v0.17.22

サンプルコードの生成

サンプルアプリであるTODO管理APIのコードを生成させるコマンドはgqlgen initといいます。
サンプルコードを配置するためのディレクトリを用意してgo mod initをしたのちに、gqlgen initを実行してみましょう。

$ go mod init my_gql_server
$ go get -u github.com/99designs/gqlgen
$ gqlgen init
Creating gqlgen.yml
Creating graph/schema.graphqls
Creating server.go
Generating...

Exec "go run ./server.go" to start GraphQL server

自動生成されたコードの解説

ここからは、gqlgen initによって生成されたコードについて解説していきます。

ディレクトリ構造

生成されたコード一覧は以下です。

.
├─ graph
│   ├─ generated.go # リゾルバをサーバーで稼働させるためのコアロジック部分
│   ├─ model
│   │   └─ models_gen.go # GraphQLのスキーマオブジェクトがGoの構造体として定義される
│   ├─ resolver.go # ルートリゾルバ構造体の定義
│   ├─ schema.graphqls # GraphQLスキーマ定義
│   └─ schema.resolvers.go # ビジネスロジックを実装するリゾルバコードが配置
├─ gqlgen.yml # gqlgenの設定ファイル
├─ server.go # サーバーエントリポイント
├─ go.mod
└─ go.sum

graph/schema.graphqls - GraphQLスキーマ定義

GraphQLにはスキーマというものが存在しており、

  • どのようなオブジェクト型が用意されているのか
  • どのようなクエリ・ミューテーションがあるのか

という情報を拡張子.graphqlsのファイルに記述しておきます。

今回gqlgen initコマンドによって生成されるのはTODO管理のサンプルアプリコードなので、そのTODO管理で使うスキーマがここに配置されています。

生成されたスキーマの内容

graph/schema.graphqls
# GraphQL schema example
#
# https://gqlgen.com/getting-started/

type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
}

type User {
  id: ID!
  name: String!
}

type Query {
  todos: [Todo!]!
}

input NewTodo {
  text: String!
  userId: String!
}

type Mutation {
  createTodo(input: NewTodo!): Todo!
}

graph/model/models_gen.go - オブジェクト構造体の定義

ToDoアプリには、

  • 追加されたTODO
  • TODOタスクのオーナーとなるユーザー

の2種類のオブジェクトが存在し、それぞれのスキーマがgraph/schema.graphqlsに定義されています。

models_gen.goファイルには、このTODOオブジェクトとユーザーオブジェクトに対応するGoの構造体型が定義されています。

graph/model/models_gen.go
type Todo struct {
	ID   string `json:"id"`
	Text string `json:"text"`
	Done bool   `json:"done"`
	User *User  `json:"user"`
}

type User struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

graph/resolver.gograph/schema.resolvers.go - リゾルバコード

サーバーサイドGraphQLのコアとなる「リゾルバ」と呼ばれる部分のボイラーテンプレートがここに生成されています。
resolver.goの中にはリゾルバ構造体Resolver型が定義されており、schema.resolvers.goの中にはメソッドが定義されています。

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

type Resolver struct{}
graph/schema.resolvers.go
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }

// CreateTodo is the resolver for the createTodo field.
func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
	panic(fmt.Errorf("not implemented: CreateTodo - createTodo"))
}

// Todos is the resolver for the todos field.
func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
	panic(fmt.Errorf("not implemented: Todos - todos"))
}

GraphQLスキーマ内で定義されていたクエリ・ミューテーションと、生成されたリゾルバメソッドの対応関係は以下のとおりです。

  • ミューテーションcreateTodo(input: NewTodo!): Todo!が呼ばれたときにCreateTodoメソッドが呼ばれる
  • クエリtodos: [Todo!]!が呼ばれたときにはTodosメソッドが呼ばれる

[再掲]GraphQLのスキーマにて定義されていたミューテーションとクエリ

graph/schema.graphqls
type Mutation {
  createTodo(input: NewTodo!): Todo!
}

type Query {
  todos: [Todo!]!
}

生成時にはpanicが入っていますが、ここを書き換えてリクエスト受信時のビジネスロジック部分を作るのがサーバーサイドGraphQLを実装する際の作業です。

graph/schema.resolvers.go
// ミューテーションcreateTodoが呼ばれた際に実行されるコード
func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
	// TODO:
	// ユーザーから受け取ったリクエスト情報inputを使ってTODOを登録し、
	// その登録されたTODOの情報をmodel.TODO型の戻り値に入れて返却
}

// クエリtodosが呼ばれた際に実行されるコード
func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
	// TODO:
	// レスポンスに含めるTODO一覧を、戻り値[]*model.Todoに入れて返却
}

graph/generated.go - リゾルバをサーバーで稼働させるためのコアロジック部分

開発者目線ではschema.resolvers.goに生成されたリゾルバメソッドの中身を埋めれば済む話ですが、そもそも

  • GraphQLのリクエストがサーバーに届く
  • リクエストに含まれているクエリを解釈し「呼ばれているのはスキーマに定義されたtodosクエリだ」と判断する
  • リゾルバ構造体のTodosメソッドを呼ぶ

という一連の判断ロジックもどこかに必要になります。

またtodosクエリの場合、開発者が書いたリゾルバメソッドの戻り値は[]model.Todo型ですが、この[]model.Todo型からユーザーに返すjsonレスポンスボディを生成する部分も、APIサーバーとして稼働させるためには必須のロジックです。

このように、

  • ユーザーから受け取ったHTTPリクエストボディに含まれているクエリから、適切なリゾルバを呼び出す
  • リゾルバが返した結果をHTTPレスポンスに変換して、ユーザーに返却する

という、リゾルバをHTTPサーバーとして稼働させるための橋渡し部分を実装しているコードがgenerated.goです。

このgenerated.goの中に生成されるコードは、GraphQLのスキーマに応じてgqlgenが自動で生成したものです。
そのため、開発者がこのファイルの中身を手動で編集・変更することはありません。

server.go - サーバーエントリポイント

GraphQLサーバーのエントリポイントとなる部分です。

  • デフォルトでは8080番ポートで稼働し、
  • /queryにGraphQLリクエストを送ったら結果が返ってきて、
  • /をブラウザで開くとクエリを実行するためのPlaygroundが使える

というものが実装されています。

生成されたエントリポイントの内容

server.go
package main

import (
	"log"
	"my_gql_server/graph"
	"net/http"
	"os"

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

const defaultPort = "8080"

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

	srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{}}))

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

gqlgen.yml

gqlgen.ymlは、gqlgenコマンドでコードを生成する際の設定を記述するyamlファイルです。
例えば、以下のような項目が設定できます。

  • スキーマに定義されたオブジェクトをGoの構造体にしたものはどこに置くのか(デフォルトでgraph/model直下のmodelパッケージ)
  • generated.goをどこに生成するのか(デフォルトでgraph直下)
  • GraphQLのスキーマファイルの置き場所(デフォルトでgraph直下にある拡張子.graphqlsファイルを使用)

生成されたgqlgen.ymlの内容(抜粋)

gqlgen.yml
# Where are all the schema files located? globs are supported eg  src/**/*.graphqls
schema:
  - graph/*.graphqls

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

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

生成されたサンプルTODO管理APIサーバーを動かしてみる

生成されたコードの紹介ができたところで、早速これを動かしてみましょう。

リゾルバの中身を埋める

まずはgraph/schema.resolvers.goに生成されたリゾルバの中身を埋めましょう。
本当はDBに接続してデータのinsertやselectクエリ実行を行うべきところですが、今回は動作を確認したいだけですので適当なTODO構造体を返すだけに留めておきます。

graph/schema.resolvers.go
// CreateTodo is the resolver for the createTodo field.
func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
-	panic(fmt.Errorf("not implemented: CreateTodo - createTodo"))
+	return &model.Todo{
+		ID:   "TODO-3",
+		Text: input.Text,
+		User: &model.User{
+			ID:   input.UserID,
+			Name: "name",
+		},
+	}, nil
}

// Todos is the resolver for the todos field.
func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
-	panic(fmt.Errorf("not implemented: Todos - todos"))
+	return []*model.Todo{
+		{
+			ID:   "TODO-1",
+			Text: "My Todo 1",
+			User: &model.User{
+				ID:   "User-1",
+				Name: "テスト",
+			},
+			Done: true,
+		},
+		{
+			ID:   "TODO-2",
+			Text: "My Todo 2",
+			User: &model.User{
+				ID:   "User-1",
+				Name: "テスト",
+			},
+			Done: false,
+		},
+	}, nil
}

サーバーを起動させる

エントリポイントであるserver.goを実行することで、GraphQLサーバーを起動させることができます。
デフォルトですと8080番ポートが使われます。

$ go run ./server.go
2023/10/17 01:07:34 connect to http://localhost:8080/ for GraphQL playground

Playgroundからクエリを実行

http://localhost:8080/をブラウザで開くと、以下のようなクエリ実行のためのPlaygroundにアクセスすることができます。

playground.png

todosクエリの実行

試しにtodosクエリを実行してみます。

query {
  todos {
    id
    text
    done
    user {
      name
    }
  }
}

result-1.png

このように、リゾルバ内で静的にreturnしていたTODO構造体の内容が取得できていることが確認できました。

createTodoミューテーションの実行

次に、createTodoミューテーションも同様に実行してみましょう。

mutation {
  createTodo(input: {
    text: "test-create-todo"
    userId: "test-user-id"
  }){
    id
    text
    done
    user {
      id
      name
    }
  }
}

result-2.png

こちらもリゾルバでreturnした内容が表示されています。これにて動作確認は成功です。

次のPart

今回はgqlgenに元々用意されていたサンプルアプリを動かしてみましたが、次からはオリジナルの内容でGraphQLサーバーを作っていきたいと思います。

以上です。
ありがとうございます。
よろしくお願いいたします。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?