はじめに

この記事は GraphQL Advent Calendar 2019 の 23 日目の記事です。(空いていたのであとから埋めました) 今回はGo/React+Typescript(hooksベース)でGraphqlスタックの管理画面を作った時の技術選定と工夫した点などを書けたらと思います。

技術選定

基本「なるべく自動生成に頼る」方針で選定しました。

サーバー(Go)

  • gqlgen: schema.graphqlからGraphQLサーバー生成くん
  • dataloaden: Dataloaderコード生成くん

フロント(CRA + Typescript)

  • gql-generator: schema.graphql から 全オペレーション xxx.gql ファイル生成君
  • graphql-codegen: オペレーション ファイルから type生成君(typescript-react-apolloのwithHooks: true) 複数の schemaファイルから生成するにはこれしかなかった

開発サイクル

  1. schema.graphql 書く
  2. サーバ
    1. gqlgen でサーバー、 I/F, Model生成する(dataloaderが必要な場合は dataloadenも使う)
    2. resolver実装する
  3. フロント
    1. gql-generatorでGraphQLオペレーション(*.gql)生成する
    2. ↑のコードを手で修正(フロントの要件に応じてネストする深さなど調節)
    3. graphql-codegenでTypescript/Reat-Apolloコード生成する

サーバー

gqlgen

schema.graphqlからGoサーバーに必要なコードを自動生成してくれるライブラリです。

まずは以下のような設定ファイルを書きます。

# gqlgen.yaml
schema:
  - ../schema.graphql
exec:
  filename: internal_gen.go
model:
  filename: model/model_gen.go
resolver:
  filename: resolver.go
  type: Resolver
autobind: []
models:
  # 自分で定義したscalarのモデルを参照する
  Date:
    model: github.com/igtm/xxx/server/model.Date
  Month:
    model: github.com/igtm/xxx/server/model.Month
  # 指定fieldのリゾルバ化設定 (lazyにさせる)
  User:
    fields:
      car: # User.car は1発で取りに行くと重いから別関数で取りに行くで
        resolver: true

ポイントは models です。カスタムのScalarのGo実装への参照指定や、リゾルバの分割設定などを書いていきます。

自動生成されるファイルを区別するために xxx_gen.go などといった命名規則にしておくといいかと思います。

dataloaden

gqlgenが勧めているDataloaderライブラリです。

上記で生成したmodelのdataloaderを作っていきます。

# dataloader生成 (名前は任意)
go run github.com/vektah/dataloaden $(NAME)$(TYPE)Loader $(TYPE) *github.com/igtm/xxx/server/model.$(NAME)

GoにはGenericsがないので、引数の型ごとに生成する必要があります。

使い方としては、middleware等でcontextにWithValueして埋め込んで適宜resolverから取り出して使う形になります。

懸念点: resolver.goだけは1度きりしか自動生成できない

resolver.goは実装を書いていくファイルなので、自動更新することができません。そのため手で関数などを作る必要があります。

懸念点: dataloadenは時間単位のbatchしかできないのが辛い

本家FacebookのNode.js実装Dataloaderは、batchしたい処理を同じTickに乗せることで過不足なく処理をまとめることができています。しかし、Goは仕組み上そのような実装方法を取ることができず、指定時間待つような微妙な実装になっています。そのため、指定時間が短すぎると、batchが実行されるのが早すぎて細切れで実行されたり、逆に長いと、無駄に時間がかかるようになります。

今回は 10ms で設定しました。

フロントについて

gql-generator + graphql-codegen を使用

gql-generator

schema.graphqlから下記コマンドで *.gql を生成します。package.jsonの scriptsなどに書いておくといいですね。

gqlg --schemaFilePath ../schema.graphql --destDirPath ./src/gql --depthLimit 5

生成されるのは、fieldが全部もりもりの *.gql なので、クライアントの要件に応じて手で適時修正します。

graphql-codegen:

schema.graphqlと *.gql から Typescript(React/Hooks) のコードを生成します。

overwrite: true
schema:
  - ../schema.graphql
documents:
  - "src/gql/**/*.gql"
generates:
  src/gql/graphql-client-api.tsx:
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-react-apollo"
      - "fragment-matcher"
    config:
      withComponent: false
      withHooks: true
      withHOC: false
      reactApolloVersion: 3

生成されたtypescriptを使った使用例はこんな感じ

import { useUserQuery } from './gql/graphql-client-api'

const userQ = useUserQuery({
  variables: {
    userID: "hoge",
  }
})

懸念点: *.gqlファイルを手で毎度修正するのが面倒

graphql-codegen で .tsファイルを生成するには

  • schema.graphql
  • *.gql

の2種類が必要になります。そのため、*.gql ファイルを手で用意して上げる必要があるのですが、毎度schemaの変更毎にこちらのファイルも変更するのは面倒です。

gql-generator を使って *gqlを自動生成していますが、フロントの要件毎に微妙に必要な情報が異なるので、手で修正する必要があります。

しかし、fieldを追加したときなど、手で修正した内容が消えてしまうのは辛いです。そこで、いいかんじに差分をチェックしてマージしてくれるshellスクリプトを作りました。

#!/bin/sh
set -e
PJROOT_DIR=$(pwd)
CONFLICT_IDENTIFIER="======="
# expected directory structure
# src/gql/queries/*.gql
# src/gql/mutations/*.gql
# ./schema.graphql
# make *.gql conflict like 'git merge'
conflictdiff () {
diff --unchanged-group-format="%=" --old-group-format="" --new-group-format="%>" --changed-group-format="<<<<<<< previous%c'\\12'%<=======%c'\\12'%>>>>>>>> new%c'\\12'" -wb $1 $2
}
export -f conflictdiff
# already conflict
if grep -qr $CONFLICT_IDENTIFIER $PJROOT_DIR/src/gql
then
echo -e "\033[31m[graphql] .gql files are conflicted. Firstly, fix below files by hand, and then try again. \033[m"
echo "conflicting files ..."
grep -nr $CONFLICT_IDENTIFIER $PJROOT_DIR/src/gql
exit 1
fi
# stash
rm -rf $PJROOT_DIR/src/gql_tmp
mv -f $PJROOT_DIR/src/gql $PJROOT_DIR/src/gql_tmp
# generate *.gql from schema.graphql using 'gql-generator'
gqlg --schemaFilePath ../schema.graphql --destDirPath ./src/gql --depthLimit 5
pushd $PJROOT_DIR/src > /dev/null
ops=(
"queries"
"mutations"
)
for op in "${ops[@]}" ; do
rm -rf gql_tmp2
mkdir -p gql_tmp2/$op
# make them conflict
( ls -l gql/$op ; ls -l gql_tmp/$op ) | awk '{print $9}' | sort | uniq -d | grep .gql | xargs -I{} sh -c "conflictdiff gql_tmp/${op}/{} gql/${op}/{} > gql_tmp2/${op}/{}; cat gql_tmp2/${op}/{} > gql/${op}/{}"
rm -rf gql_tmp2
done
popd > /dev/null
rm -rf $PJROOT_DIR/src/gql_tmp
# print if *.gql conflicts
if grep -qr $CONFLICT_IDENTIFIER $PJROOT_DIR/src/gql
then
echo -e "\033[31m[graphql] .gql files are conflicted. fix below files by hand. \033[m"
echo "conflicting files ..."
grep -nr $CONFLICT_IDENTIFIER $PJROOT_DIR/src/gql
exit 0
fi

出力形式は、git mergeでコンフリクトした時にできるアレ(>>>>>>>>)です。VSCodeだと簡単に修正できるので、以下のようにポチポチ押すだけで楽なので、その書式に沿うようにしてます。

vscode-gql-diff

まとめ

今回は gqlgen/graphql-codegenを使うことでschemaファイルからほとんど自動でコード生成することができました。

また、 *gqlファイルもスクリプト使えばいい感じに更新できるようになりました。

この環境でどんどん開発していきたいなと思ってます。

またgqlgenでロール権限管理した話もどこかでしたいと思ってます。

gophers by Renee French CC BY 3.0