投稿と再現手順を、まとめて保存する。グリッチハンターのDB設計
ゲームのバグを投稿するアプリには、タイトルや説明だけでなく、「どう操作すると、その現象が起きるか」という再現手順も必要になる。
Glitch Hunter Libraryのコードを読み返すと、投稿と手順を別のテーブルに保存し、両方の書き込みを一つのトランザクションにまとめていた。今回は、その実装をたどってみる。

画像:Prisma公式サイトの共有用画像。
企画から制作までの経緯は、以前の記事に書いた。実装はAIと進めた。この記事では、Prismaスキーマと投稿APIをもとに、保存の流れと、今後見直したい点を整理する。
対象はリポジトリの1f5e948時点。今回はコードとマイグレーションを読み、本番DBへの適用状況や実行時の動作までは確認していない。
一つの投稿に、複数の手順を持たせる
DBはPostgreSQL、アプリからの操作にはPrismaを使っている。
主な関係はこうなっている。
| テーブル | 保存する内容 | 関係 |
|---|---|---|
Glitch | 投稿のタイトル、ゲーム名、説明など | 投稿の本体 |
ReproductionStep | 手順番号、操作内容、動画内の時刻など | 一つの投稿に複数件 |
Vote | ユーザーIDと投稿ID | ユーザーと投稿の組み合わせ |
再現手順を分けておくと、「一番目の操作」「二番目の操作」という単位で扱える。ReproductionStepはglitch_idで投稿を参照し、step_numberに順番を持っている。
表示時にも、この番号で並べる必要がある。DBに番号を保存しただけで、取得結果が自動的に番号順になるわけではない。
重複させたくない組み合わせを、DBに書く
手順のモデルには、次の指定がある。
@@unique([glitch_id, step_number])同じ投稿に、同じ番号の手順を二つ保存しないための制約だ。投稿Aの手順1と投稿Bの手順1は保存できるが、投稿Aの手順1を重複させることはできない。
投票のモデルにも、組み合わせに対する指定がある。
@@unique([user_id, glitch_id])これは、一つのユーザーIDと一つの投稿IDの組み合わせを重複させない。ただし、一人が複数アカウントを使うことまで防ぐ仕組みではない。また、操作している本人がそのユーザーなのかを確かめる認証・認可も別に必要になる。
「一人一票」という言葉だけでまとめず、DBが実際に制限しているのはどの値の組み合わせかを確認したい。PostgreSQLの制約の説明も、この区別を考える際の参考になる。
途中で失敗したら、投稿だけ残さない
投稿APIは、最初にGlitchを作り、そのIDを使って再現手順を作成する。この二つの処理は、Prismaの$transactionの中に置かれている。
流れを文章にすると、次のようになる。
- 投稿の本体を作る。
- 作成した投稿IDを、それぞれの手順へ付ける。
- 手順をまとめて保存する。
- 全体が成功したら、保存を確定する。
手順を保存している途中でエラーになった場合、投稿本体だけが残ることを防ぐ構成だ。API側では、受け取った手順の配列からindex + 1で番号を付けている。
ただし、手順が渡されなければ、手順なしの投稿を作ることもできる。「必ず手順付きで投稿される」という制約にはなっていない。
必要なら、配列の型、手順の件数、文字数、空の操作説明などを入力時に検証する必要がある。トランザクションが守るのは保存処理のまとまりであり、内容の妥当性まですべて保証するわけではない。
通知は、保存のあとに送っている
DBへの保存が終わったあと、Discordへの通知処理を呼び出している。通知に失敗した場合はログに記録し、APIは作成した投稿を返す構成になっている。
通知先の不調で、保存済みの投稿まで失敗扱いにしない、という分け方だ。
一方、この呼び出しは待ち合わせておらず、永続的なキューもこの箇所にはない。実行環境がレスポンス後に処理を止める場合や、一時的な通信失敗があった場合の配信保証はない。
通知を必ず届ける必要があるなら、保存と一緒に通知待ちの記録を残し、別の処理で再送できるようにする設計が候補になる。これは今後の改善案で、実装済みの機能ではない。
動いた先に、説明できる範囲を増やす
今回読んだ範囲では、投稿と手順をまとめて保存し、番号の重複をDBで制限し、外部通知を保存後に分けていることが分かった。
次に検証するなら、手順の保存を意図的に失敗させたときに投稿本体が残らないこと、制約違反をどう利用者へ返すか、通知に失敗しても投稿を確認できることを、ローカルの検証用DBで確かめたい。今回はそこまでの実行検証はしていない。
AIと作ったアプリでも、保存が途中で止まるとどうなるのか、どこで重複を防ぐのか、外部サービスが止まるとどうなるのかは、コードへ戻って説明できるようにしたい。
画面からは見えないけれど、運用を続けるときに必要になる部分だと思う。
確認したソース: