Day17 後半
「消す」のではなく「どう残すか」まで含めて設計する
前半で、DELETE が
「本当に行そのものを消す、最も重い一手」
だというところまでは来ました。
後半では、もう一歩踏み込んで、
- 外部キー(親子関係)があるときの DELETE の挙動
- 物理削除(DELETE)と論理削除(フラグ)の使い分け
- ログや監査情報は「基本消さない」という設計
- セキュリティ的に“消してはいけないもの”の考え方
を扱っていきます。
ここからは、「DELETE をどう書くか」よりも
「そもそも何を DELETE していいのか」
という設計の話が中心になります。
親子関係と DELETE
親を消したとき、子はどうなるのか
現実のデータベースでは、テーブル同士が「親子関係」を持つことが多いです。
例えば、次のような関係を考えます。
users テーブル(親)
orders テーブル(子。users.id を参照)
users
id | name
---+-----------
1 | 山田太郎
2 | 佐藤花子
orders
id | user_id | amount
---+---------+-------
1 | 1 | 1200
2 | 1 | 3000
3 | 2 | 500
ここで、「users.id = 1 を DELETE したら、orders の user_id=1 の行はどうなるのか?」
という問題が出てきます。
外部キー制約(FOREIGN KEY)をきちんと設定している場合、
SQLite では大きく分けて次のような挙動を選べます。
親を消そうとしたらエラーにする(子が残っている限り消させない)
親を消したら、子も一緒に消す(ON DELETE CASCADE)
どちらを選ぶかは設計次第ですが、
セキュリティ・監査の観点では、
「履歴やログまで一緒に消えてしまう」のはかなり危険
ということを強く意識しておく必要があります。
ON DELETE CASCADE の怖さと便利さ
「一発で全部消える」ことの意味
外部キーに ON DELETE CASCADE を付けると、
親を DELETE したときに、子も自動的に DELETE されます。
例えば、users に対して orders が ON DELETE CASCADE でぶら下がっているとします。
DELETE FROM users
WHERE id = 1;
SQLとすると、users.id=1 の行が消えるだけでなく、
orders.user_id=1 の行も自動的に全部消えます。
これは、
ユーザーを消したら、そのユーザーの注文も全部消えてほしい
という要件にはとても便利です。
しかし、セキュリティ・監査の観点では、
「誰が何を買ったか」という履歴が丸ごと消える
不正な削除が起きたとき、痕跡ごと消えてしまう
というリスクがあります。
だから実務では、
ユーザー本体は論理削除(フラグ)
注文やログは物理削除しない(DELETE しない)
という設計を選ぶことが多いです。
物理削除と論理削除
DELETE するか、フラグで「消えたことにする」か
ここで、よく出てくる二つの考え方を整理します。
物理削除
テーブルから行そのものを DELETE する。
ストレージ上からも消える。
戻すにはバックアップやログが必要。
論理削除
行は残したまま、フラグで「消えた扱い」にする。
例えば is_deleted や deleted_at を持たせる。
アプリ側の SELECT で「is_deleted = 0 のものだけ見る」ようにする。
例として、users に is_deleted 列を持たせます。
id | name | is_deleted
---+------------+-----------
1 | 山田太郎 | 0
2 | 佐藤花子 | 0
3 | 鈴木一郎 | 1
「id=3 を削除したい」とき、物理削除なら
DELETE FROM users
WHERE id = 3;
SQLですが、論理削除なら
UPDATE users
SET is_deleted = 1
WHERE id = 3;
SQLとします。
アプリ側では、
SELECT * FROM users
WHERE is_deleted = 0;
SQLのように、「削除されていないものだけ」を見るようにします。
論理削除のメリットは、
後から「やっぱり復活させたい」となったときに戻せる
誰がいつ削除したかを別テーブルに記録しやすい
監査・セキュリティ上、「消した履歴」を残せる
という点です。
一方で、データ量が増えやすい、クエリが少し複雑になる、
といったデメリットもあります。
ログや監査テーブルは「基本 DELETE しない」という発想
消す対象と、絶対に消さない対象を分ける
セキュリティの世界では、
「ログは資産であり、証拠であり、最後の砦」
という考え方があります。
例えば、
login_logs(ログイン履歴)
audit_logs(操作履歴)
security_events(セキュリティ関連イベント)
のようなテーブルは、
基本的に DELETE しない、あるいは極めて限定的にしか DELETE しない、
という運用が多いです。
どうしても古いデータを整理したい場合でも、
一定期間より古いものを、別のアーカイブテーブルに移す
バックアップを取ってから、物理削除する
といった慎重な手順を踏みます。
ここで大事なのは、
「何でもかんでも DELETE してはいけない」
「テーブルごとに、“消していいもの”と“消してはいけないもの”を分ける」
という設計の視点です。
「消す前に残す」ための設計
削除ログ・履歴テーブルという考え方
もう一歩踏み込むと、
DELETE する前に、その行の情報を別テーブルにコピーしておく
という設計もあります。
例えば、users を物理削除する前に、
deleted_users というテーブルに退避しておくイメージです。
- 削除対象を確認する
SELECT * FROM users
WHERE id = 3;
SQL- その行を deleted_users に INSERT する(アプリ側でやることが多い)
- 最後に DELETE を実行する
DELETE FROM users
WHERE id = 3;
SQLこうしておけば、
「誰をいつ消したか」「消される前はどんな状態だったか」
を後から追うことができます。
SQLite 単体で完全自動化するのは少し工夫が要りますが、
アプリケーション側で「削除前に必ず履歴に書く」というルールを作るのは現実的です。
セキュリティの視点から見る「消しすぎない勇気」
消すことより、「残すべきものを残す」ことが大事
攻撃者にとっての DELETE は「証拠隠滅の道具」です。
ログを消す
特定ユーザーの履歴だけ消す
不正操作の痕跡を消す
こうしたことを防ぐために、
システム設計側はあえて
ログテーブルには DELETE 権限を与えない
アプリケーションからは DELETE できないようにする
削除はバッチ処理や専用ツールからのみ行う
といった制限をかけます。
Day17 の締めとして、覚えておいてほしいのはこれです。
DELETE を覚えたからといって、
「何でも消せるようになった!」ではなく、
「何を消してよくて、何を絶対に消すべきでないかを考えるフェーズに入った」
ということです。
小さな練習イメージ
頭の中で、次の日本語をどう設計するか考えてみてください。
ユーザーは論理削除(is_deleted フラグ)にし、注文履歴は物理削除しない。
ログイン履歴テーブルは、アプリケーションからは DELETE できないようにする。
1年以上前のログを、削除する前にアーカイブテーブルに移してから消す。
どれも、「DELETE をどう打つか」だけでなく、
「そもそもどのテーブルに対して、どのレベルの削除を許すか」という設計の話になっているはずです。
Day17 後半のまとめ
外部キーと DELETE の組み合わせでは、「親を消したときに子をどうするか」(エラーにするか、巻き込んで消すか)が重要な設計ポイントになる。
物理削除(DELETE)と論理削除(フラグ)は、それぞれメリット・デメリットがあり、監査や復元を重視するなら論理削除が有力な選択肢になる。
ログや監査テーブルは「基本 DELETE しない」「消す前に必ずどこかに残す」という発想が、セキュリティ上とても大事。
DELETE は「消す技術」ではなく、「何を残すかまで含めて考えるためのきっかけ」だと捉えると、設計の質が一段上がる。
ここまで来たあなたは、
単に SQL を書けるだけでなく、
「データをどう生かし、どこまで残し、どこから消すか」を考えられる段階に入っています。
この先の制約設計やトランザクションの話とも、きれいにつながっていきます。
