家計簿アプリを作る #90:Cloudflare Pages への再デプロイ(外部キー制約の適用)
家計簿アプリ作成シリーズの第90回です。前回、アプリ側のテーブルに外部キー制約とON DELETE CASCADEを付け、アカウント削除から手動の削除処理を取り除きました。今回はこれを本番環境へ反映します。テーブルを作り直すマイグレーションを含むので、いつもより慎重に進めました。
順番:マイグレーションが先、コードが後
今回はマイグレーションの適用とコードのデプロイの順番が重要でした。
もし先にコードだけをデプロイすると、本番のDBにはまだ外部キー制約がありません。その状態でアカウントが削除されると、userの行だけが消え、取引やカテゴリなどのデータが持ち主のいないまま全部残ります。手動削除を取り除いた直後の、いちばん危険な状態です。
そのため、本番のD1にマイグレーションを適用し、制約が入ったことを確認してから、コードをpushしました。
バックアップ
テーブルを作り直す移行なので、適用前にバックアップを取ることにしました。
wrangler d1 exportの認証エラー
まずDB全体のダンプを試しましたが、認証エラーになりました。
✘ [ERROR] A request to the Cloudflare API (.../d1/database/.../export) failed.
Authentication error [code: 10000]
同じ出力の中に、Wrangler自身が次の警告を出していました。
▲ [WARNING] Wrangler is missing some expected Oauth scopes.
To fix this, run `wrangler login` to refresh your token.
ログイン時に発行されたOAuthトークンが古く、今のWranglerが期待する権限を満たしていない状態です。npx wrangler loginでトークンを取り直すのが第一の対処で、それでも駄目な場合は、ダッシュボードでD1の編集権限を持つAPIトークンを作り、CLOUDFLARE_API_TOKEN環境変数に設定して実行する方法があります。
Time Travelとアプリのバックアップ機能
d1 exportが使えなくても、備える手段は他にもあります。
D1には、過去30日間の任意の時点に復元できるTime Travelという仕組みが標準で付いています。移行前に現在のブックマークを控えておけば、失敗しても戻せます。d1 exportと違ってDBを止める必要もありません。
npx wrangler d1 time-travel info kakeibo-app-db
npx wrangler d1 time-travel restore kakeibo-app-db --bookmark 控えたブックマーク
お題85で作ったアプリのバックアップ機能も使えます。ただし対象はアプリ側のテーブルだけで、Better Authのuser・session・accountは含まれない点に注意が必要です。
本番の孤児データ確認でつまずいたこと
適用前に、本番でも孤児データが無いかを確認しました。ここで2回エラーを出しています。
WEHREのタイプミス
near "user_id": syntax error at offset 40: SQLITE_ERROR
原因はWHEREをWEHREと打っていたことでした。SQLiteはFROM transactions WEHREを「transactionsテーブルにWEHREという別名を付けた」と解釈し、その直後に来たuser_idで構文が壊れた、という流れです。エラーが指している位置は、実際にタイプミスした単語ではなくその次の単語になるので、少し分かりにくいパターンです。
D1の複合SELECTの上限
7テーブル分を1回で確認しようと、UNION ALLで7つのSELECTを繋げたところ、別のエラーになりました。
too many terms in compound SELECT: SQLITE_ERROR
D1では、UNIONで繋げられる項の数に上限があり、7つは多すぎました。各テーブルの件数を、1つのSELECTの中にスカラーサブクエリとして列で並べる形に変えると通りました。
SELECT
(SELECT COUNT(*) FROM transactions WHERE user_id NOT IN (SELECT id FROM user)) AS transactions,
(SELECT COUNT(*) FROM categories WHERE user_id NOT IN (SELECT id FROM user)) AS categories,
-- ...残り5テーブルも同様
これは複合SELECTではなく、1つのSELECTに7つの列があるだけなので、上限に引っかかりません。結果は全テーブル0件でした。
マイグレーションの適用と確認
npx wrangler d1 migrations apply kakeibo-app-db --remote
本番のD1ではPRAGMA foreign_keys=OFFが効かないので、前回書き換えたPRAGMA defer_foreign_keys = onが意味を持つのはここが初めてです。エラーは出ませんでした。
適用後、テーブル定義に制約が入ったことを確認しました。
npx wrangler d1 execute kakeibo-app-db --remote --command "SELECT sql FROM sqlite_master WHERE type='table' AND name='transactions'"
デプロイと本番での動作確認
制約が入ったことを確認してから、アカウント削除の変更をpushしてデプロイしました。
本番の自分のアカウントは使わず、確認用のアカウントを作って確かめました。
- 確認用アカウントで取引とクイック登録のテンプレートを登録する
- 削除前にユーザーIDを控える
- 設定画面からアカウントを削除する
- 控えたIDのデータが残っていないことを確認する
npx wrangler d1 execute kakeibo-app-db --remote --command "SELECT COUNT(*) FROM transaction_templates WHERE user_id = '控えたID'"
前回ローカルで確認したときは、アカウント削除にまだ手動の削除処理が残っていたので、userの行を直接削除してカスケードを確かめました。今回デプロイしたコードには手動削除が無いので、画面からアカウントを削除してデータが消えれば、それがそのまま本番でカスケードが効いている証明になります。件数は0で、問題なく動作しました。
まとめ
| ポイント | 内容 |
|---|---|
| 適用の順番 | 手動削除を消したコードは、外部キー制約が入った後にデプロイする |
d1 exportの認証エラー |
OAuthトークンが古い可能性。wrangler loginで取り直すか、APIトークンを使う |
| Time Travel | ブックマークを控えておけば、DBを止めずに過去の状態へ戻せる |
| SQLのエラー位置 | タイプミスした単語ではなく、その次の単語でエラーが報告されることがある |
| D1の複合SELECTの上限 | UNION ALLを多数繋げる代わりに、スカラーサブクエリを列として並べる |
| 本番での検証 | 手動削除が無い状態で画面から削除すれば、それがカスケードの検証になる |