取消は追記、削除じゃない
「削除」ボタンを押すと成功レスポンスが返るのに、一覧をリロードしても消えない。 このバグは大抵、書き込みは合っているが読み取りが古いままという形で起きる。
起きるパターン
監査要件があるテーブルは、行を物理削除せず「取消を表す追記」で表現することが多い。 仕訳・注文・承認ログなど、あとから「本当に消えたのか」を追跡したい対象は軒並みこの形になる。
物理削除しない理由は明快(監査ログが残る)だが、その代償として 「現在有効かどうか」の判定はテーブルを見るだけでは終わらない。 リビジョン側の種別まで見て初めて、今そのレコードが生きているかが決まる。
Supabaseでの除外クエリ
current_revision_id が指す revision_kind を直接JOINで絞れないときの回避策。
// 1. 取消済みリビジョンのIDを先に取る
const { data: cancelledRevisionRows } = await supabase
.from('journal_revisions')
.select('id')
.eq('company_id', companyId)
.eq('revision_kind', 'cancellation');
// 2. current_revision_id がそのID群に含まれる行を除外する
const cancelledRevisionIds = (cancelledRevisionRows || []).map((r) => r.id);
if (cancelledRevisionIds.length > 0) {
const idList = cancelledRevisionIds.map((id) => `"${id}"`).join(',');
query = query.not('current_revision_id', 'in', `(${idList})`);
countQuery = countQuery.not('current_revision_id', 'in', `(${idList})`);
}
.not(column, 'in', '(id1,id2,...)') は文字列を自前で組み立てる必要がある。
UUIDなら引用符の混入は心配しないでよいが、取消件数が増えるとURL長・クエリ長が伸びる。
件数が数百を超える運用なら、DB側にビューを切るか current_revision_id の参照先へ
revision_kind を非正規化して持たせるほうが安全。
除外条件は一覧と件数の両方に書く
一覧クエリだけ直して件数クエリを直し忘れると、ページネーションの表示件数と 実際に並ぶ行数がずれる。「全120件」と出ているのに数えると118件、という形で気づかれる。 2つのクエリを持つ実装では、除外条件を定数や共通関数に切り出して両方から呼ぶ。
抜けを洗い出す手順
このバグはコンパイルもLintも通る。見つけるには実際に操作するしかない。
- 対象データを1件、取消操作する
- APIレスポンスが成功していることを確認する(ここで安心して止まりやすい)
- 画面をリロードして、一覧・件数の両方を目視する
- 消えていなければ、一覧・件数クエリの
WHERE条件を洗う
3を飛ばして2で止まると、「書き込みは直したのに、また同じ不具合」という 形で再発する。書き込み経路のPRと読み取り経路のPRが分かれているときほど起きやすい。
実際に踏んだ経緯
ちょうぼっちの仕訳削除で、確認ダイアログ撤廃や月次一括コピーの削除失敗を 複数のPRで直したあとも、選択削除した仕訳が一覧に残り続けた。 経緯は 仕訳の削除、直したのに直ってなかった話 に書いた。