取消は追記、削除じゃない

更新: 2026-09-15
supabasepostgresql論理削除監査ログ

「削除」ボタンを押すと成功レスポンスが返るのに、一覧をリロードしても消えない。 このバグは大抵、書き込みは合っているが読み取りが古いままという形で起きる。

起きるパターン

監査要件があるテーブルは、行を物理削除せず「取消を表す追記」で表現することが多い。 仕訳・注文・承認ログなど、あとから「本当に消えたのか」を追跡したい対象は軒並みこの形になる。

対象レコード(journals)current_revision_id → rev_1取消操作rev_2 を追記revision_kind = cancellationcurrent_revision_id を更新一覧クエリ: revision_kindを見ていない→ rev_2はcancellationなのに、行はそのまま一覧に出る
取消は既存行を消さず、新しいリビジョンを積んで現在地を差し替えるだけ。一覧側の除外条件は別物として書く必要がある。

物理削除しない理由は明快(監査ログが残る)だが、その代償として 「現在有効かどうか」の判定はテーブルを見るだけでは終わらない。 リビジョン側の種別まで見て初めて、今そのレコードが生きているかが決まる。

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つのクエリを持つ実装では、除外条件を定数や共通関数に切り出して両方から呼ぶ。

一覧クエリ(fetchJournals).not('current_revision_id', 'in', ...)件数クエリ(countQuery)同じ条件を書き忘れやすい同じ除外条件を両方に
片方だけ直すと「全120件」の表示と実際に並ぶ行数がずれる。除外条件は共通化して両方から呼ぶ。

抜けを洗い出す手順

このバグはコンパイルもLintも通る。見つけるには実際に操作するしかない。

  1. 対象データを1件、取消操作する
  2. APIレスポンスが成功していることを確認する(ここで安心して止まりやすい)
  3. 画面をリロードして、一覧・件数の両方を目視する
  4. 消えていなければ、一覧・件数クエリのWHERE条件を洗う

3を飛ばして2で止まると、「書き込みは直したのに、また同じ不具合」という 形で再発する。書き込み経路のPRと読み取り経路のPRが分かれているときほど起きやすい。

実際に踏んだ経緯

ちょうぼっちの仕訳削除で、確認ダイアログ撤廃や月次一括コピーの削除失敗を 複数のPRで直したあとも、選択削除した仕訳が一覧に残り続けた。 経緯は 仕訳の削除、直したのに直ってなかった話 に書いた。

WIKI一覧へ