はじめに
React + Hono + Cloudflare Workers + D1 で作っているアプリに、コメントへのいいね機能を追加しました。
本番には反映できていた一方で、ローカル環境を開くとコメント欄だけが表示されず、「サーバーで問題が発生しました」と出ました。
画面自体は開けていて、投稿一覧も取得できています。つまり、アプリ全体が落ちているわけではなく、コメント取得まわりだけで問題が起きている状態でした。
この記事では、どうやって調べたか、どこを見て原因を特定したか、どう対処したか、最後にどう確認したかをまとめます。
問題
発生していた問題は、コメント一覧APIが 500 Internal Server Error になっていたことです。
画面は表示されますが、コメント欄だけが失敗していました。
原因切り分け
まず、フロントエンドとAPIのコンテナが本当に起動しているかを確認しました。
docker-compose ps
結果として、frontend と api はどちらも起動していました。
kinkyo-note-api-1 Up 0.0.0.0:8787->8787/tcp
kinkyo-note-frontend-1 Up 0.0.0.0:5173->5173/tcp
次に、ブラウザから到達しているかを確認しました。
curl -i http://localhost:5173/g/hanchu/feed
curl -i http://localhost:8787/api/health
curl -i http://localhost:5173/api/health
いずれも 200 OK が返りました。
フロントエンドの画面もAPIのヘルスチェックも動いているため、ポートやDockerの起動失敗ではないと考えました。
そこで、APIログを確認しました。
docker-compose logs --tail=200 api
ログには次のエラーが出ていました。
DrizzleQueryError: Failed query:
select "comment_id", count("id")
from "classmate_comment_likes"
where "classmate_comment_likes"."comment_id" in (?)
group by "classmate_comment_likes"."comment_id"
cause: Error: D1_ERROR: no such table: classmate_comment_likes: SQLITE_ERROR
ここで原因がかなり絞れました。
アプリのコードは classmate_comment_likes テーブルを読みに行っていまが、ローカルD1にはそのテーブルが存在していません。
つまり、コードの問題というより、ローカルDBに新しいマイグレーションが適用されていない状態でした。
今回追加したコメントいいね機能では、次のテーブルを使います。
export const classmateCommentLikes = sqliteTable(
'classmate_comment_likes',
{
id: integer('id').primaryKey({ autoIncrement: true }),
commentId: integer('comment_id')
.notNull()
.references(() => classmateComments.id, { onDelete: 'cascade' }),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
},
)
Drizzle のマイグレーションファイルも存在していました。
drizzle/0008_famous_bloodaxe.sql
中身を見ると、確かに classmate_comment_likes を作るSQLが含まれています。
CREATE TABLE `classmate_comment_likes` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`comment_id` integer NOT NULL,
`user_id` integer NOT NULL,
`created_at` integer NOT NULL,
FOREIGN KEY (`comment_id`) REFERENCES `classmate_comments`(`id`) ON DELETE cascade,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade
);
ここまでで分かったことは、次の通りです。
- 画面とAPIサーバーは起動している
- ヘルスチェックも成功している
- コメント一覧APIだけが500になっている
- APIログに
no such table: classmate_comment_likesが出ている - 該当テーブルを作るマイグレーションファイルは存在している
- したがって、ローカルD1にマイグレーションが未適用である可能性が高い
ポイントは、最初からDBを疑ったわけではないことです。
まず起動状態、次に疎通、最後にAPIログという順で見たことで、問題の範囲を狭められました。
解決方法
ローカルD1に未適用のマイグレーションを当てました。
今回実行したコマンドは次です。
docker-compose exec api npx wrangler d1 migrations apply kinkyo-note-db --local --persist-to .wrangler/state
docker-compose exec api を使っているのは、APIコンテナ内で使っているローカルD1の状態に対して操作したいためです。
--local は、Cloudflare上の本番D1ではなく、ローカルD1へ適用する指定になります。
--persist-to .wrangler/state は、ローカルD1の保存場所を指定しています。
このプロジェクトで、wrangler dev も同じディレクトリを使ってローカルD1を永続化しているので、マイグレーション適用先とAPI起動時のD1保存先がずれると、適用したはずなのにアプリから見るとテーブルがない、という状態になります。
docker-compose.yml でも、API起動時に同じ指定を使っています。
command: >
sh -c "npm ci &&
npx wrangler d1 migrations apply kinkyo-note-db --local --persist-to .wrangler/state &&
npm run dev:api -- --ip 0.0.0.0 --persist-to .wrangler/state"
なぜ今回漏れたのか
コンテナが以前から起動しっぱなしだったためです。
docker-compose up の起動コマンドにはマイグレーション適用が入っています。ただし、すでに起動しているコンテナに対して新しいマイグレーションファイルを追加しても、その起動コマンドが自動でもう一度走るわけではありません。
そのため、コードだけが新しくなり、ローカルD1のテーブルは古いまま残っていました。
対処後、Wrangler は次のように 0008_famous_bloodaxe.sql を適用しました。
Migrations to be applied:
0008_famous_bloodaxe.sql
5 commands executed successfully.
0008_famous_bloodaxe.sql ✅
ローカルDBを完全に作り直したい場合は、Compose の volume ごと削除する方法もあります。
docker-compose down -v
docker-compose up
ただし、この方法はローカルD1のデータも消えます。
今回は既存のローカルデータを残したかったので、未適用のマイグレーションだけを当てる方法にしました。
おわりに
対処後、落ちていたコメントAPIを直接確認しました。
curl -i http://localhost:5173/api/groups/hanchu/classmates/1/comments
curl -i http://localhost:5173/api/groups/hanchu/classmates/9101/comments
どちらも 200 OK になりました。
GET /api/groups/hanchu/classmates/1/comments 200 OK
GET /api/groups/hanchu/classmates/9101/comments 200 OK
レスポンスには、コメント本文だけでなく、コメントいいね用の値も含まれていました。
{
"comments": [
{
"id": 27,
"classmateId": 1,
"body": "やっほー",
"canLike": true,
"likeCount": 0,
"likedByMe": false
}
],
"nextCursor": null
}
最後にブラウザをリロードすると、コメント欄も表示できる状態に戻りました。
今回の学びは、ローカル環境でも「コード」と「DBスキーマ」は別物だということです。
TypeScript のコードや Drizzle の schema を追加しても、D1に実際のテーブルが作られていなければ、実行時には no such table で落ちます。
特に Docker Compose で開発環境を長く起動しっぱなしにしている場合、起動時にマイグレーションを実行する仕組みがあっても、新しく追加した分は反映されていないことがあります。