GoのgRPCサーバーとGraphQLサーバーを繋げてみる【GraphQL/gqlgen】
初めての技術記事投稿です。
まだまだ初学者の身ですが、どんどんアウトプットしていきたい所存です!
本来は一つの記事にまとめようと考えていましたが、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リポジトリで公開しています。
GraphQLサーバーの実装
別Bookにて作成したarticleサービスを扱うことのできるGraphQLサーバーを立てていきます。
1. GraphQLのスキーマ定義
まずはルート配下にgraphディレクトリを作成します。
そして、その中にschema.graphqlsファイルを作成していきます。
.
└─ graph
└── schema.graphqls
schema.graphqlsには、articleサービスのCRUD+全取得を扱うスキーマを定義していきます。
# 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サーバーとやり取りできるようにします。
package graph
import "github.com/k88t76/GraphQL-gRPC-demo/article/client"
type Resolver struct {
ArticleClient *client.Client
}
4. articleClientの前処理
resolverでの処理を行うコードが冗長になってしまうことを避けるために、articleサービスのclientに処理を施します。
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
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
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
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
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
}
全取得
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を登録すれば完成です。
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サーバーを起動します。
go run article/server/server.go
>>> Listening on port 50051...
次に、GraphQLサーバーを起動します。
go run graph/server/server.go
>>> connect to http://localhost:8080/ for GraphQL playground
ブラウザで http://localhost:8080/ にアクセスするとGraphQLのplaygroundに遷移します。
様々なクエリを発行してみましょう。
自分で作成したサービスだとplaygroundがより一層面白いと思います!
データベースへの保存は以下のコマンドで確認できます。
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