GraphQLの基本
公開日:2024-10-04
はじめに
- GraphQLは以前より業務で利用する機会があり、基本的な概念は理解していたが実際に自身で実装したことがなかったので、GraphQLの基本を手を動かして学習しました。
- Query, Mutation, Subscriptionの基本的な使い方を学習し、BBSのバックエンドを参考にGraphQLサーバーを構築してみました。
GraphQLとは
- APIのための問い合わせ言語
- クライアント/サーバ通信のための言語仕様
GraphQLの特徴
- データ取得のQuery, データの変更のMutation, データの変更の監視のSubscriptionの3つの操作が可能
- APIはRESTのエンドポイントの集合でなく、型の集合。新しいAPIを作成する前に、APIのデータ型を定義する必要があり、このデータ型の集合をスキーマと呼ぶ。
- スキーマにデータ型を定義するのに対して、データを取得する作業はリゾルバの役割。
Quick Start
- gqlgenというGolangのGraphQLライブラリを利用して、GraphQLサーバーを構築します。
-
go moduleの初期化
mkdir gqlgen_practice cd gqlgen_practice go mod init gqlgen_practice
-
gqlgenを追加
printf '//go:build tools\npackage tools\nimport (_ "github.com/99designs/gqlgen"\n _ "github.com/99designs/gqlgen/graphql/introspection")' | gofmt > tools.go go mod tidy # add missing and remove unused modules
-
gqlgenの初期化
go run github.com/99designs/gqlgen init go mod tidy
-
graphqlのサーバーを起動
go run server.go
-
http://localhost:8080/
にアクセスして、GraphQL Playgroundが表示されれば成功 -
ディレクトリについて
. ├── go.mod ├── go.sum ├── gqlgen.yml # gqlgenコマンドでコードを生成する際の設定を記述するyamlファイル ├── graph │ ├── generated.go # リゾルバをサーバーで稼働させるためのコアロジック部分(gqlgenで自動生成) │ ├── model │ │ └── models_gen.go # GraphQLのスキーマオブジェクトがGoの構造体として定義される(gqlgenで自動生成) │ ├── resolver.go # ルートリゾルバ構造体の定義 │ ├── schema.graphqls # GraphQLのスキーマ定義 │ └── schema.resolvers.go # ビジネスロジックを実装するリゾルバコードが配置 ├── server.go # サーバーのエントリーポイント └── tools.go
バックエンドの実装
- 以前REST APIで実装したBBSのバックエンドをGraphQLに移行してみます。
- Github 出来上がったものはこちらのリポジトリにまとめています。
-
ディレクトリの作成
# リポジトリのクローン git clone [email protected]:ys39/Gin_bbs.git bbs-gql-project cd bbs-gql-project # リポジトリ設定 git remote set-url origin [email protected]:ys39/Gin_gql_bbs.git
-
モジュール名の変更
go mod edit -module bbs-gql-project
-
gqlgenの追加
go install github.com/99designs/[email protected] go get -u github.com/99designs/[email protected] go run github.com/99designs/gqlgen init # gqlgenの初期化
-
スキーマを記述
-
BBSへの投稿、取得、更新、削除の機能についてのスキーマを記述
vim graph/schema.graphqls
type Post { id: ID! title: String! content: String! } type Query { getAllPosts(page: Int!, per_page: Int!): [Post!]! getPost(id: ID!): Post! } input NewPost { title: String! content: String! } input updatePost { title: String! content: String! } type Mutation { createPost(input: NewPost!): Post! updatePost(id: ID!, input: updatePost!): Post! deletePost(id: ID!): Boolean! }
- ディレクトリ構成
-
下記ファイルは必要ないため削除しました
- controllers/
- views/
- openapi.yaml
- server.go
-
結果的に下記のようなディレクトリ構成になりました
├── README.md ├── go.mod ├── go.sum ├── gqlgen.yml ├── graph │ ├── generated.go │ ├── model │ │ └── models_gen.go │ ├── resolver.go │ ├── schema.graphqls # スキーマ定義 │ └── schema.resolvers.go # リゾルバの実装 ├── main.go # サーバーのエントリーポイント ├── models │ ├── error.go # エラーレスポンスの定義 │ └── post.go # Postの構造体定義, 返すデータを定義 ├── routers │ └── router.go # ルーティングの設定 └── test └── graph_test.go # リゾルバのテスト
- リゾルバの実装
-
graph/schema.resolvers.go
にリゾルバの実装を記述package graph import ( "bbs-gql-project/graph/model" "bbs-gql-project/models" "context" "strconv" ) // 投稿の詳細取得のリゾルバ func (r *queryResolver) GetPost(ctx context.Context, id string) (*model.Post, error) { postID, err := strconv.Atoi(id) if err != nil { return nil, models.BadRequestError("invalid ID format", "invalid ID format") } for _, post := range models.Posts { if post.ID == postID { return &model.Post{ ID: strconv.Itoa(post.ID), Title: post.Title, Content: post.Content, }, nil } } return nil, models.NotFoundError("post not found", "post not found") } // Mutation returns MutationResolver implementation. func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } // Query returns QueryResolver implementation. func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } type mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver }
- エラーレスポンスの定義
-
GraphQLのレスポンスのmessageとして返すエラーメッセージを定義
package models import ( "fmt" "net/http" ) // カスタムエラー構造体 type AppError struct { Code int `json:"code"` // HTTPステータスコード Message string `json:"message"` // エラーメッセージ Detail string `json:"detail"` // エラー詳細 } // Errorメソッドを実装して、エラーメッセージを返す func (e *AppError) Error() string { return fmt.Sprintf("code: %d, message: %s, detail: %s", e.Code, e.Message, e.Detail) } // 新しいエラーを作成 func NewAppError(code int, message string, detail string) *AppError { return &AppError{ Code: code, Message: message, Detail: detail, } } // 404 Not Found func NotFoundError(message string, detail string) *AppError { return NewAppError(http.StatusNotFound, message, detail) } // 400 Bad Request func BadRequestError(message string, detail string) *AppError { return NewAppError(http.StatusBadRequest, message, detail) } // 500 Internal Server Error func InternalServerError(message string, detail string) *AppError { return NewAppError(http.StatusInternalServerError, message, detail) }
- ファイル生成
-
graph/generated.go
とgraph/model/models_gen.go
を生成するために以下のコマンドを実行go run github.com/99designs/gqlgen generate go mod tidy
-
実行と確認
go run main.go # `http://localhost:8080/v1/gql/` へアクセスして下記を実行し、データ取得や更新ができるかを確認する
-
getPostクエリで投稿の詳細を取得
query getPost { getPost(id: 1) { id title content } }
-
deletePostミューテーションで投稿を削除
mutation deletePost { deletePost(id: 3) }
-
getAllPostsクエリで投稿一覧を取得
query getAllPosts { getAllPosts(page: 1, per_page: 10) { id title content } }
-
updatePostミューテーションで投稿を更新
mutation updatePost { updatePost(id: 4, input: { title: "qqq", content: "qqq2" }) { id title content } }
-
createPostミューテーションで投稿を作成
mutation createPost { createPost(input: { title: "hoge", content: "peco" }) { id title content } }
-
テストの実行
# テスト cd ~/bbs-gql-project/test go test [GIN] 2024/10/04 - 19:07:40 | 200 | 183.993µs | | POST "/v1/gql/query" [GIN] 2024/10/04 - 19:07:40 | 200 | 30.487µs | | POST "/v1/gql/query" [GIN] 2024/10/04 - 19:07:40 | 200 | 255.297µs | | POST "/v1/gql/query" [GIN] 2024/10/04 - 19:07:40 | 200 | 34.103µs | | POST "/v1/gql/query" [GIN] 2024/10/04 - 19:07:40 | 200 | 22.703µs | | POST "/v1/gql/query" PASS ok bbs-gql-project/test 0.004s
全体的な所感
- まだ
go mod
あたりの使い方が身についておらず、gqlgen
を使う際にも少し手間取っている。 go run main.go
で起動したPlay Groundでクエリを実行することで、データの取得や更新ができるかを確認できるので、開発がしやすい。(途中でPostmanを利用してみたが、localhost:8080のアクセスに対してconnect ECONNREFUSED 127.0.0.1:8080
のエラーが発生してしまい、うまくいかなかった。)- REST APIの際はコントローラーを作成して、エンドポイントに対して処理を記述していたが、GraphQLの場合はリゾルバを作成して、スキーマに対して処理を記述するのでディレクトリ構成が少し異なる。ここのベストプラクティスを学びたい。
まとめ
- GraphQLの基本的な使い方を
gqlgen
を用いて学習し、BBSのバックエンドをGraphQLに移行してみました。 - スキーマファースト or コードファーストのどちらがいいのかなどの考察ができていないので、今後はそのあたりも学習していきたいです。