家計簿アプリを作る #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してデプロイしました。

本番の自分のアカウントは使わず、確認用のアカウントを作って確かめました。

  1. 確認用アカウントで取引とクイック登録のテンプレートを登録する
  2. 削除前にユーザーIDを控える
  3. 設定画面からアカウントを削除する
  4. 控えた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を多数繋げる代わりに、スカラーサブクエリを列として並べる
本番での検証 手動削除が無い状態で画面から削除すれば、それがカスケードの検証になる
← トップページに戻る