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 にアクセスしてお薬データとお薬カテゴリを取得するリクエストを投げてみます

スクリーンショット 2022-08-15 21.28.56.png (224.4 kB)

無事に取得できています。
発行されたSQLは以下です。

QueryContext args=["1"] query="SELECT * FROM medicines where id = $1"
QueryContext args=["1"] query="SELECT * FROM medicine_categories where id = $1"

次に、お薬データのみ取得してみます。 スクリーンショット 2022-08-15 21.28.22.png (196.2 kB)

こちらも想定通りお薬データの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サーバーを構築してみようという方、是非一度使ってみてはいかがでしょうか!