TL;DR
- 開発している Slack bot で発生する N+1 問題を解消するために GitHub GraphQL API v4 を利用した
- クライアントサイド(今回は JavaScript)側から使ってみただけだが、かなり開発体験が良かった
背景: review-waiting-list-bot について
review-waiting-list-bot という Slack bot を開発し、今もメンテナンスしている。
ざっくり言うと以下のように動いていた
- GitHub REST API v3 の Search API を叩く
- ユーザーの入力した条件に応じてフィルタリングする
- 結果を整形する
- Slack に通知する
詳細は以下の記事を参照
今回はこのプロセスのうちの 1 を、 GitHub GraphQL API v4 を使ってややスマートにできたという話をする。
requested reviewer でフィルタしたい
GitHub issue に requested reviewer でフィルタしたいという要望*1が来た。requested reviewer は pull request page の以下の UI から設定できる情報のことだ。
フィルタの追加は過去にもやった*2ので最初は簡単だと思ったのだが、Search API の返すフィールドに requested reviewer の情報が含まれておらずどうしたものかと少し悩んだ。この情報を取得するための Review Requests API もあるのだがこのエンドポイントを pull request ごとに呼び出すと bot の処理がたいへん重くなってしまう(クライアントサイドの N+1 問題)*3。
これが GraphQL だと1リクエストでバシッと必要な情報だけ取得できたりするのだろうかと思い至り、まずは https://developer.github.com/v4/explorer/:title) で試しにクエリを書き始めてみた。するとあっさり必要な情報だけを expose するクエリが書けてしまった。
赤い四角で囲んだ箇所が欲しかった情報だ。
JavaScript から GraphQL API を利用する
クエリが書けた、つまりやりたいことが GraphQL を使って実装できるとわかった。あとはこのクエリを JavaScript、正確に言うと Node.js のコードとして埋め込むだけだ。
GraphQL API の呼び出し方は 4 simple ways to call a GraphQL API – Apollo GraphQL を参考にした。HTTP クライアントは promisify したかった & 使ったことあるというだけで axios にしたので実質何でも良い。
骨子は以下のようになった。
const axios = require("axios") const client = axios.create({ baseURL: 'https://api.github.com/', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': `Bearer ${process.env.GITHUB_AUTH_TOKEN}`, }, }) const query = ` query { search(first:100, query:"type:pr author:ohbarye state:open", type: ISSUE) { nodes { ... on PullRequest { title, url, author { login, }, labels(first:100) { nodes { name, }, }, reviewRequests(first:100) { nodes { requestedReviewer { ... on User { login } ... on Team { name } } } } } } } }` const response = await this.client.post('graphql', { query }) console.log(response.data) // This results below /* { "data": { "search": { "nodes": [ { "title": "Enable to fetch pull requests by specifying assignee", "url": "https://github.com/ohbarye/review-waiting-list-bot/pull/26", "author": { "login": "ohbarye" }, "labels": { "nodes": [ { "name": "enhancement" } ] } } ] } } } */
response の中身も graphiql 上で確認できているので統合もスムーズにできた。
(実際には GraphQL の導入と requested reviewer によるフィルタ機能の追加は別PRにした)
残課題
- ページング
pageInfo.hasNextPage
やpageInfo.(start|end)Cursor
を使って実装できることはわかるが、配列要素がネストしている場合が謎- クライアント側の実装がハチャメチャに複雑にならないか?
- エラー処理
- フィールドを区切るカンマは不要?
- クエリ内のダブルクォーテーションの中にダブルクォーテーションをさらに埋め込むことはできない?
感想
- 今回の実装で受けられた恩恵は一般的に語られているものの再確認であり、"真髄"に迫るような感じではなかった
- N+1 を解消できた
- 必要なフィールドのみにアクセスすることでレスポンスの content size を減らせた
- とはいえ API が返却するリソースのモデル定義を容易に、かつインタラクティブに確認できるのはクライアントを実装するうえでかなり良い開発体験だった
- 雰囲気でやっているのでベストプラクティス的なものを知りたい
- サーバサイドの実装はまったくわからない
*1:https://github.com/ohbarye/review-waiting-list-bot/issues/32
*2:正確にはやってもらったtomoima525/support label by tomoima525 · Pull Request #23 · ohbarye/review-waiting-list-bot · GitHub
*3:worker 処理なのでタイムアウトなどの心配はないのだが Slack で bot を呼び出してから応答が遅くなるのは決して良くない