TypeScript を使って GraphQL API を作る。
とりあえず簡単な Todo アプリのバックエンドを想定して作る。
アプリのシップではなく、設計や実装のノウハウを手に入れることが目的。
cp .env.example .env
docker compose up -d
corepack enable
pnpm install
pnpm migrate:reset
pnpm dev
クエリの実行は Web コンソール で。
token を Authorization ヘッダへ Bearer でセットしておくこと。
token は seed スクリプト から取得する。
Prisma Studio は pnpm studio
で起動しておくこと。
データの部分的な取得をサポートする為、基本的に nullable とした。
取得できなかったフィールドに対するフォールバックはクライアントが決める。
- クライアントが部分取得を求めていないことがわかっている
- フィールドの欠けたデータが意味を成さない
等の場合は non-nullable とする。
resolveInfo の解析により DB からの取得列を絞れそうだが、コードの複雑化に対する恩恵が小さいと判断し、許容することにした。
Relay の GraphQL Server Specification を満たすため、node interface を用意した。
node リゾルバで後続のリゾルバを決定する必要があるため、ID へタイプを表すプレフィックスを付加することにした。
UUIDv4, UUIDv7, ULID, Cuid2, Nano ID, 連番が候補に挙がったが、
- 時系列の値であること
- 分散環境での衝突耐性があること
- 安定運用の実績があること
を重視し、ULID を採用した。
時系列の値を求める理由は btree インデックスのキャッシュヒット率を上げるため。
参考: MySQL でプライマリキーを UUID にする前に知っておいて欲しいこと
PostgreSQL でも同様と考えた。
UUIDv7 は正式リリース&安定運用されたら採用可能。
連番は衝突の心配がない環境なら採用可能。
いくらかのパフォーマンスと引き換えに、API へ柔軟性をもたらす技術という認識。
スキーマ設計やリゾルバの実装、周辺ツールの利用などなにかと習熟が必要だが、それに見合う価値はあると思う。
ところで nullable という概念に optional と null の意味が混在しているのは扱いにくいと思う。
データソースを扱うツール。
Language agnostic にスキーマを定義し、マイグレーション出来る。
TS であればスキーマ定義をもとに型付きのクライアントを生成出来る。
別の言語向けに生成するサードパーティーライブラリもあるよう。
今回は下記理由によりクライアントの使用を避けた。
- SQL が汚い
- バッチ化の自由度が低い
- ライブラリのサイズが大きいのでデプロイ環境を選ぶ
- マルチに使える分 API がややわかりにくい
特にバッチ化の自由度が低いのは致命的で、
{
users(first: 30) {
nodes {
todos(first: 50) {
nodes {
id
}
}
}
}
}
上記クエリにおいて、各 User の すべての Todo を読み込んでオンメモリで件数を絞り込むよう。各 User の Todo の件数が大きい場合、著しいオーバーヘッドが発生する。効率的に読み込むには、各 User の Todo を first 件数分だけ取得する SELECT 文を UNION によって結合する必要がある。
今回は DB クライアントに kysely を使ったが、UNION で複数の SELECT 文を結合する機能が欠けているようなので @prisma/client と状況は変わらない。残念です…。