🥺

GoのgRPCサーバーとGraphQLサーバーを繋げてみる【GraphQL/gqlgen】

2021/04/12に公開

初めての技術記事投稿です。
まだまだ初学者の身ですが、どんどんアウトプットしていきたい所存です!

本来は一つの記事にまとめようと考えていましたが、gRPCサーバーを立てるところだけでボリューミーになったのでその部分は以下Bookにて記載しています。

gRPCサーバー構築編:GoでgRPCサーバーを立ててみる

今回やること

  • Goで簡単なgRPCサーバーを立てる(別Book参照)
  • gqlgenを用いてGraphQLサーバーを立て、gRPCサーバーとやり取りする(本記事で扱います)

今回やらないこと

  • Dockerの利用
  • gRPC、GraphQLの概念説明

構成

アーキテクチャ

GraphQLサーバーに複数のサービスを繋ぐマイクロサービス構成が一般的だと思いますが、今回は1つだけサービスを繋ぎます。(次回以降でマイクロサービスっぽい構築に発展させていきたい)

ディレクトリ

  • article ・・・ gRPCを用いたCRUDな記事投稿サービス
  • graph ・・・ gqlgenを用いたGraphQLサービス
.
├── article
│   ├── client
|   ├── pb
|   ├── repository
|   ├── server
|   ├── service
|   ├── article.proto
|   └── article.sql
|
├── graph
│   ├── generated
|   ├── model
|   ├── server
|   ├── resolver.go
|   ├── schema.graphqls
|   └── schema.resolvers.go
|
├── go.mod
├── go.sum
└── gqlgen.yml

経緯

自作アプリのバックエンドをRESTからgRPCに変えようと考えましたが、フロント側(TypeScript)とgRPCでやり取りするのが大変そうだと感じました。
そこで、BFF(Backend For Frontend)としてGraphQLサーバーを介入させればフロント側と容易にやり取りできるかもしれないと思い、この構成に至りました。

また、この構成に関する日本語文献が多くないと感じたため記事にしようと考えました。
同じ構成を試みる誰かに、この記事を見つけてもらえたらいいなと思います!

サンプルコード

実装したものは以下のGitHubリポジトリで公開しています。
https://github.com/k88t76/GraphQL-gRPC-demo

GraphQLサーバーの実装

別Bookにて作成したarticleサービスを扱うことのできるGraphQLサーバーを立てていきます。

1. GraphQLのスキーマ定義

まずはルート配下にgraphディレクトリを作成します。
そして、その中にschema.graphqlsファイルを作成していきます。

.
└─ graph
   └── schema.graphqls

schema.graphqlsには、articleサービスのCRUD+全取得を扱うスキーマを定義していきます。

graph/schema.graphqls
# articleサービスとやり取りするArticleの型定義
type Article {
    id: Int!
    author: String!
    title: String!
    content: String!
}

# CREATEのためのinputを定義
input CreateInput {
    author: String!
    title: String!
    content: String!
}

# UPDATEのためのinputを定義
input UpdateInput {
    id: Int!
    author: String!
    title: String!
    content: String!
}

# mutationの定義(CREATE, UPDATE, DELETEを行う)
type Mutation {
    createArticle(input: CreateInput!): Article!
    updateArticle(input: UpdateInput!): Article!
    deleteArticle(input: Int!): Int!
}

# queryの定義(article → READ, articles → 全取得)
type Query {
    article(input: Int!): Article!
    articles: [Article!]!
}

2. コード生成

それでは、コードの生成を行なっていきましょう。
go.modが無い場合はルート配下にて作成しておきます。

go mod init

また、gqlgenのGraphQLサービスに必要なパッケージをインストールしておきましょう。

go get github.com/99designs/gqlgen \
       github.com/vektah/gqlparser/v2

インストールできたらルート配下で以下のコマンドを実行しましょう。

go run github.com/99designs/gqlgen init

実行後、いろいろと生成されると思います。

├── graph
│   ├── generated - generated.go (自動生成)
|   ├── model - models_gen.go (自動生成)
|   ├── resolver.go (自動生成)
|   ├── schema.graphqls
|   └── schema.resolvers.go (自動生成)
|
├── go.mod
├── go.sum
├── gqlgen.yml (自動生成)
└── server.go (自動生成)
  • generated.go・・・GraphQLサービスを実装するためのもの。行数が多い。
  • models_gen.go・・・ schemaファイルで定義した型が記述されている。
  • resolver.go・・・ Resolverの型定義を行うところ。
  • schema.resolvers.go・・・ Resolverで行う処理を実装するところ。
  • gqlgen.yml・・・ 自動生成における依存関係などが記述されている。
  • server.go・・・ GraphQLサーバーを立てるためのもの。

3. Resolverの定義

resolver.goにarticleサービスのclientを実装し、gRPCサーバーとやり取りできるようにします。

graph/resolver.go
package graph

import "github.com/k88t76/GraphQL-gRPC-demo/article/client"

type Resolver struct {
	ArticleClient *client.Client
}

4. articleClientの前処理

resolverでの処理を行うコードが冗長になってしまうことを避けるために、articleサービスのclientに処理を施します。

article/client/client.go
func (c *Client) CreateArticle(ctx context.Context, input *pb.ArticleInput) (*model.Article, error) {
	// CREATE処理のレスポンスを受け取る
	res, err := c.Service.CreateArticle(
		ctx,
		&pb.CreateArticleRequest{ArticleInput: input},
	)
	if err != nil {
		return nil, err
	}
	
	// GraphQLサービスで扱える形にしてCREATEしたArticleを返す
	return &model.Article{
		ID:      int(res.Article.Id),
		Author:  res.Article.Author,
		Title:   res.Article.Title,
		Content: res.Article.Content,
	}, nil
}

func (c *Client) ReadArticle(ctx context.Context, id int64) (*model.Article, error) {
	// READ処理のレスポンスを受け取る
	res, err := c.Service.ReadArticle(ctx, &pb.ReadArticleRequest{Id: id})
	if err != nil {
		return nil, err
	}
	
	// GraphQLサービスで扱える形にしてREADしたArticleを返す
	return &model.Article{
		ID:      int(res.Article.Id),
		Author:  res.Article.Author,
		Title:   res.Article.Title,
		Content: res.Article.Content,
	}, nil
}

func (c *Client) UpdateArticle(ctx context.Context, id int64, input *pb.ArticleInput) (*model.Article, error) {
	//UPDATE処理のレスポンスを受け取る
	res, err := c.Service.UpdateArticle(ctx, &pb.UpdateArticleRequest{Id: id, ArticleInput: input})
	if err != nil {
		return nil, err
	}
	
	// GraphQLサービスで扱える形にしてUPDATEしたArticleを返す
	return &model.Article{
		ID:      int(res.Article.Id),
		Author:  res.Article.Author,
		Title:   res.Article.Title,
		Content: res.Article.Content,
	}, nil
}

func (c *Client) DeleteArticle(ctx context.Context, id int64) (int64, error) {
	// DELETE処理のレスポンスを受け取る
	res, err := c.Service.DeleteArticle(ctx, &pb.DeleteArticleRequest{Id: id})
	if err != nil {
		return 0, err
	}
	// DELETEしたArticleのIDを返す
	return res.Id, nil
}

func (c *Client) ListArticle(ctx context.Context) ([]*model.Article, error) {
	// 全取得処理のレスポンスを受け取る
	res, err := c.Service.ListArticle(ctx, &pb.ListArticleRequest{})
	if err != nil {
		return nil, err
	}
	
	// GraphQLサービスで扱える形にして全取得した記事をスライスとして返す
	var articles []*model.Article
	for {
		r, err := res.Recv()
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, err
		}
		articles = append(articles, &model.Article{
			ID:      int(r.Article.Id),
			Author:  r.Article.Author,
			Title:   r.Article.Title,
			Content: r.Article.Content,
		})
	}
	return articles, nil
}

これでarticleClientにおける処理は完了です!

5. Resolverの処理を実装

まずはmutationでの処理を実装していきます。

CREATE

graph/schema.resolvers.go
func (r *mutationResolver) CreateArticle(ctx context.Context, input model.CreateInput) (*model.Article, error) {
	// gRPCサーバーでArticleをCREATE
	article, err := r.ArticleClient.CreateArticle(
		ctx,
		&pb.ArticleInput{
			Author:  input.Author,
			Title:   input.Title,
			Content: input.Content,
		})
	if err != nil {
		return nil, err
	}

	// CREATEしたArticleを返す
	return article, nil
}

UPDATE

graph/schema.resolvers.go
func (r *mutationResolver) UpdateArticle(ctx context.Context, input model.UpdateInput) (*model.Article, error) {
	// gRPCサーバーでArticleをUPDATE
	article, err := r.ArticleClient.UpdateArticle(
		ctx,
		int64(input.ID),
		&pb.ArticleInput{
			Author:  input.Author,
			Title:   input.Title,
			Content: input.Content,
		})
	if err != nil {
		return nil, err
	}

	// UPDATEしたArticleを返す
	return article, nil
}

DELETE

graph/schema.resolvers.go
func (r *mutationResolver) DeleteArticle(ctx context.Context, input int) (int, error) {
	// gRPCサーバーでArticleをDELETE
	id, err := r.ArticleClient.DeleteArticle(ctx, int64(input))
	if err != nil {
		return 0, err
	}

	// DELETEしたArticleのIDを返す
	return int(id), nil
}

次にqueryでの処理を実装していきます。

READ

graph/schema.resolvers.go
func (r *queryResolver) Article(ctx context.Context, input int) (*model.Article, error) {
	// 入力したIDの記事をgRPCサーバーからREAD
	article, err := r.ArticleClient.ReadArticle(ctx, int64(input))
	if err != nil {
		return nil, err
	}

	// READしたArticleを返す
	return article, nil
}

全取得

graph/schema.resolvers.go
func (r *queryResolver) Articles(ctx context.Context) ([]*model.Article, error) {
	// gRPCサーバーでArticleを全取得
	articles, err := r.ArticleClient.ListArticle(ctx)
	if err != nil {
		return nil, err
	}

	// 全取得したArticleを返す
	return articles, nil
}

これでResolverの処理が実装できました!

6. GraphQLサーバーの構築

今回はgraphディレクトリ内でサーバーを立てる処理を実装していくので、自動生成されたserver.goを移動させます。

mkdir graph/server
mv server.go graph/server/

GraphQLサーバーにarticleクライアントを実装したResolverを登録すれば完成です。

graph/server/server.go
package main

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

	"github.com/99designs/gqlgen/graphql/handler"
	"github.com/99designs/gqlgen/graphql/playground"
	"github.com/k88t76/GraphQL-gRPC-demo/article/client"
	"github.com/k88t76/GraphQL-gRPC-demo/graph"
	"github.com/k88t76/GraphQL-gRPC-demo/graph/generated"
)

const defaultPort = "8080"

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

	// articleClientを生成
	articleClient, err := client.NewClient("localhost:50051")
	if err != nil {
		articleClient.Close()
		log.Fatalf("Failed to create article client: %v\n", err)
	}

	// GraphQLサーバーに先程のResolverを実装
	srv := handler.NewDefaultServer(
		generated.NewExecutableSchema(
			generated.Config{
				Resolvers: &graph.Resolver{
					ArticleClient: articleClient,
				}}))

	// GraphQL playgroundのエンドポイント
	http.Handle("/", playground.Handler("GraphQL playground", "/query"))

	// 実装したクエリが実行可能なGraphQLサーバーのエンドポイント
	http.Handle("/query", srv)

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

これで、GraphQLサーバーが起動できるようになりました!

7. 動作確認 playgroundで遊ぼう!

まずは、gRPCサーバーを起動します。

1:zsh
go run article/server/server.go
>>> Listening on port 50051...

次に、GraphQLサーバーを起動します。

2:zsh
go run graph/server/server.go
>>> connect to http://localhost:8080/ for GraphQL playground

ブラウザで http://localhost:8080/ にアクセスするとGraphQLのplaygroundに遷移します。

様々なクエリを発行してみましょう。
自分で作成したサービスだとplaygroundがより一層面白いと思います!

データベースへの保存は以下のコマンドで確認できます。

3:zsh
sqlite3 article/article.sql
sqlite>
sqlite> select * from articles;
>>> 1|Gopher|gRPC|gRPC is so cool!
    2|GraphQL master|GraphQL|GraphQL is very smart!

以上で今回の目的である「GraphQLサーバーにgRPCサーバーを繋ぐ」が達成できました!

おわりに

この構成を試してみて、gRPCもgqlgenも定義したスキーマをもとに開発を行うのでスムーズな開発が可能になると感じました。
いわゆる「スキーマ駆動開発」の利点を体感できたと思います。
次回以降ではGraphQLサーバーに複数のサービスを繋いだ"マイクロサービス"っぽいものを実装していきたいと考えています。

拙い点もあったと思いますが、ここまで読んでいただきありがとうございました!

参考文献

Discussion