入力リスト外を暗黙削除する sync/set API は「削除だけ全件成功ゲート」で守る
バックエンド
設計判断
API設計
知識
判断
「渡されたリストを最終状態として同期し、リスト外の既存レコードを削除する」sync/set 型の API では、削除が「入力に現れなかったこと」という消極的な事実から導かれるため、要素の取りこぼし・縮退・部分失敗がそのままデータ削除に化ける。通常の一括操作で有効な「要素独立の部分成功」を削除にまで適用すると危険になる。
削除に化ける具体経路
- 部分失敗でも削除を実行: 他人 id や不正要素が failed になった入力でも、残りが成功すると「failed になった参照先」がリスト外扱いされ削除される。
- 入力の縮退: id フィールドの型不正(数値のつもりの文字列等)を検証層が null に縮退すると、「更新のつもりの要素」が「新規作成」に化け、元のレコードがリスト外として消える(重複作成と削除が同時に起きる最悪形)。
- クライアント側の drop&送信: 不正要素を落として正常分だけ送る方式だと、落とした要素の参照先がサーバー側でリスト外削除される。
守り方(3層)
- 削除だけ全件成功ゲート: 作成・更新は要素独立の部分成功でよいが、リスト外削除は「全要素が成功したときのみ」実行する。1件でも失敗があれば削除をスキップし、削除件数を空で報告する。
- id 保存 sync: 全削除→再構築(delete-rebuild)は既存 id が変わり外部参照を壊す。id 有り→更新、id 無し→作成、リスト外→削除の差分同期にする。作成/更新の分岐は「id キーの有無」で判定し、値の真偽値判定(非ゼロ判定)で代用しない。id キーが存在するのに型が不正なら当該要素を失敗にする(作成への縮退禁止)。
- 削除フェーズの原子性: 対象の確定(スナップショット)と削除を同一トランザクションで行い、行ロック等で同一ユーザーの並行 sync を直列化する(並行する2つの sync が互いの保持分を消し合うレース対策)。削除失敗は握り潰さず全ロールバックして失敗を応答に反映する。
クライアント側を書ける場合はさらに all-valid-or-nothing(ローカル検証で1件でも不正なら送信しない)にすると、サーバーゲートと二重の守りになる。
検証方法(識別反証)
- 他人 id の failed と新規作成成功を混在させ、削除が一切発生しないこと(「成功1件以上で削除実行」の fail-open 実装はここで落ちる)。
- id を文字列にした型不正要素を混ぜ、当該が失敗になり既存レコードが残存すること(null 縮退→作成化け実装はここで落ちる)。
- 更新成功時に id が変わらないこと(delete-rebuild 実装はここで落ちる)。
- 削除失敗注入時に他の削除がロールバックされ失敗が応答に現れること。