GraphQLライブラリ「gqlgen」でサーバー構築
こんにちは。株式会社PRVENT開発部、バックエンドチームの横川です。
今回は弊社で使用しているGraphQLライブラリ 「gqlgen」と実際にgqlgenを使ったGraphQLサーバーの構築方法を紹介したいと思います。
なぜ書こうと思ったか
弊社のプロダクトのバックエンド開発でどんな技術を使っているか知ってもらいたいと思い、使用している主要ライブラリの一つ「gqlgen」を紹介することにしました。
最初に弊社のgqlgen使用状況とgqlgen, GraphQLについて軽く説明した後、実際にgqlgenを使いサーバーの構築してGraphQLの特徴について説明できたらと思います。
弊社のgqlgen使用状況
現在以下3つのプロダクトのバックエンドでgqlgenを採用してます
- Mystar(生活習慣記録アプリ)
- MystarPRO(利用者さんの進捗管理システム)
- Mymonitor(取引先健保用システム)
弊社ではバックエンドにRubyとGoを使ったシステムがありますが現在Goのシステムは全てgqlgenを採用しています。
gqlgenとは
GraphQLサーバーを構築するためのライブラリです。(github: https://github.com/99designs/gqlgen)
自分は他のライブラリを使ったことがないので比較できないですが、GraphQLライブラリには「コードファースト」と「スキーマファースト」の二種類があり、gqlgenは「スキーマファースト」のライブラリです
ちなみに GraphQL とは
GraphQLは、APIのクエリ言語であり、データ用に定義した型システムを使用してクエリを実行するためのサーバー側ランタイムです。
引用: https://graphql.org/learn/
公式にはこう書かれています。
実際の流れとして、リクエストボディに取得したいデータをクエリ言語で定義してサーバーへリクエストを投げると、取得したいデータだけjsonで返ってきます。
画像はクライアントツールを使ったリクエスト(左)とレスポンス(右)です。
下記のようにJSON形式でリクエストも可能です。
$ curl -H "Content-Type:application/json" -X POST http://localhost:8080/query -d '{ "query" : "query { users{ id, name }}" }' {"data":{"users":[{"id":"1","name":"横川"}]}}
REST APIだと、取得したいデータによってエンドポイントを変えるかと思いますが、GraphQLだとエンドポイントは一つで取得するデータをクライアント側で指定できます。
このようなGraphQLサーバーをgqlgenを使い簡単に構築できます。
gqlgenを使ってサーバー構築
gqlgenを使ってお薬データを取得するGraphQLサーバーを構築してみます。
まずはgithubのクイックスタートを参考に、gqlgenの初期化コマンド実行まで進めます。
mkdir gqlgen-medicines cd gqlgen-medicines go mod init gqlgen-medicines printf '// +build tools\npackage tools\nimport _ "github.com/99designs/gqlgen"' | gofmt > tools.go go mod tidy go run github.com/99designs/gqlgen init
いくつかファイルが自動生成されているかと思いますが、今回編集するのは以下の3ファイルです。
- graph/schema.resolver.go
- リクエストを元に実際の処理を実装するresolverファイル。
- graph/schema.graphqls
- GraphQLスキーマを定義します。このファイルをもとに他のファイルのコードが再生成されます。
- gqlgen.yml
- gqlgenの設定ファイルです。
まず、graph/schema.graphqlsを編集します。
今回は以下のようにしました。
type Query { Medicine(ID: ID!): MedicineDetail! } # お薬 type MedicineDetail { ID: ID! name: String! medicneCategory: MedicineCategoryDetail! } # お薬カテゴリ type MedicineCategoryDetail { ID: ID! name: String! }
- IDを引数に、お薬データを一件取得するクエリを定義してます。
- type.MedicineDetail(お薬)はMedicineCategoryDetail(お薬カテゴリ)と紐づいています。
続いて、gqlgen.ymlファイルを以下のように編集します。
# 省略 models: ID: model: - github.com/99designs/gqlgen/graphql.ID - github.com/99designs/gqlgen/graphql.Int - github.com/99designs/gqlgen/graphql.Int64 - github.com/99designs/gqlgen/graphql.Int32 Int: model: - github.com/99designs/gqlgen/graphql.Int - github.com/99designs/gqlgen/graphql.Int64 - github.com/99designs/gqlgen/graphql.Int32 # ここから↓追記 MedicineDetail: fields: medicneCategory: resolver: true
- ここで、query.MedicineでmedicneCategory(お薬カテゴリ)を取得する際に呼ばれるメソッド生成の設定をしてます。
ここまで一度 go run github.com/99designs/gqlgen generate
を実行し schema.resolvers.go ファイルのコードを再生成します。
メソッド自体は自動生成されるので中の処理を実装していきます。
package graph // This file will be automatically regenerated based on the schema, any resolver implementations // will be copied through when generating and any unknown code will be moved to the end. import ( "context" "database/sql" "gqlgen-medicines/graph/generated" "gqlgen-medicines/graph/model" "os" _ "github.com/lib/pq" "github.com/rs/zerolog" sqldblogger "github.com/simukti/sqldb-logger" "github.com/simukti/sqldb-logger/logadapter/zerologadapter" ) const dbConn string = "host=localhost port=5432 user=postgres password=password dbname=gqlgen_sample sslmode=disable" type Medicine struct { ID string Name string MedicineCategoryID string } type MedicineCategory struct { ID string Name string } func dbConnection() *sql.DB { db, err := sql.Open("postgres", dbConn) if err != nil { panic(err) } return db } // sqlログ出力設定 func logConf(db *sql.DB) *sql.DB { logger := zerolog.New( zerolog.ConsoleWriter{Out: os.Stdout, NoColor: false}, ) db = sqldblogger.OpenDriver( dbConn, db.Driver(), zerologadapter.New(logger), ) return db } // MedicneCategory is the resolver for the medicneCategory field. func (r *medicineDetailResolver) MedicneCategory(ctx context.Context, obj *model.MedicineDetail) (*model.MedicineCategoryDetail, error) { db := dbConnection() defer db.Close() db = logConf(db) var m MedicineCategory err := db.QueryRow("SELECT * FROM medicine_categories where id = $1", obj.MedicneCategory.ID).Scan(&m.ID, &m.Name) if err != nil { return nil, err } return &model.MedicineCategoryDetail{ ID: m.ID, Name: m.Name, }, nil } // Medicine is the resolver for the Medicine field. func (r *queryResolver) Medicine(ctx context.Context, id string) (*model.MedicineDetail, error) { db := dbConnection() defer db.Close() db = logConf(db) var m Medicine err := db.QueryRow("SELECT * FROM medicines where id = $1", id).Scan(&m.ID, &m.Name, &m.MedicineCategoryID) if err != nil { return nil, err } return &model.MedicineDetail{ ID: m.ID, Name: m.Name, MedicneCategory: &model.MedicineCategoryDetail{ ID: m.MedicineCategoryID, }, }, nil } // MedicineDetail returns generated.MedicineDetailResolver implementation. func (r *Resolver) MedicineDetail() generated.MedicineDetailResolver { return &medicineDetailResolver{r} } // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } type medicineDetailResolver struct{ *Resolver } type queryResolver struct{ *Resolver }
query.Medicineが呼ばれた時の処理はfunc (r *queryResolver) Medicine(...)
で、medicneCategoryを取得する処理は func (r *medicineDetailResolver) MedicneCategory(...)
です
これでGraphQLサーバーの構築は完了です。
リクエストを投げてみる
GraphQLサーバーの構築ができたのでリクエストを投げてみます。
以下のようなDBを用意しました。
medicine_categories(お薬カテゴリテーブル)
ID | name |
---|---|
1 | 頭痛薬 |
medicines(お薬テーブル)
ID | name | medicine_category_id |
---|---|---|
1 | ロキソニン | 1 |
go run server.go
でサーバーが起動するのでhttp://localhost:8080 にアクセスしてお薬データとお薬カテゴリを取得するリクエストを投げてみます
無事に取得できています。
発行されたSQLは以下です。
QueryContext args=["1"] query="SELECT * FROM medicines where id = $1" QueryContext args=["1"] query="SELECT * FROM medicine_categories where id = $1"
次に、お薬データのみ取得してみます。
こちらも想定通りお薬データのIDとnameのみ取得できてます。
発行されたSQLは以下です。
QueryContext args=["1"] query="SELECT * FROM medicines where id = $1"
お薬カテゴリを省いたことで、func (r *medicineDetailResolver) MedicneCategory(...)
が呼ばれなくなりました。
REST APIだとエンドポイントごとで返すリソースが決まっているので、お薬データだけが必要なページでもお薬カテゴリの取得が発生してしまいます。
GraphQLだとお薬カテゴリが必要なページでのみリクエストに加えれば良いので、無駄なSQLの発行を防ぐことができます。
まとめ
今回は弊社で使用しているGraphQLライブラリ「gqlgen」を使い、構築したサーバにリクエストを投げてみて、そこから分かるGraphQLの特徴を一つ紹介しました。
GoでGraphQLサーバーを構築してみようという方、是非一度使ってみてはいかがでしょうか!