VillageSQL is a drop-in replacement for MySQL with extensions.
All examples on this page work on VillageSQL. Install Now →
基本パターン
NULL 許容のタイムスタンプを追加します。NULL は有効、タイムスタンプは削除済みを意味し、いつ削除されたかを記録します。
deleted ブール値よりもタイムスタンプを選んでください。コストは同じで、サポートチケットが必ず尋ねる「いつ」に追加コストなしで答えられます。
問題 1: 一意制約
email に対する一意インデックスは、再登録を永久にブロックします。ソフトデリートされた行がまだその値を占有しているためです。
NULL を許容するため、削除済みの行が衝突することはなく、同じメールアドレスを持つ 2 つの有効な行は引き続き失敗します。仕組みについては生成列を参照してください。
問題 2: すべてのクエリが覚えていなければならない
WHERE deleted_at IS NULL を 1 つ忘れるだけで、削除済みのデータがレポートや API レスポンスに漏れます。緩和策は 2 つあります。
- 有効な行のサブセットのビュー。 読み取り経路をビューに向ければ、フィルタを忘れることはできません。
- ORM のスコープ。 ほとんどの ORM は、フィルタをグローバルに適用できます(Rails の
default_scope、Django のマネージャー、Prisma のミドルウェア)。すべてのクエリを監査する代わりに、1 か所で有効にします。
ON DELETE CASCADE は決して発火しません。子が親と一緒に消えるべきなら、アプリケーション(またはトリガー)が自ら deleted_at をカスケードしなければなりません。
ソフトデリートすべきでない場合
- 規制上の消去。 GDPR の削除要求は、データが隠されるのではなく消えることを意味します。ソフトデリートされた行も個人データとして数えられます。
- 入れ替わりの激しいテーブル。 セッション、キューのジョブ、ログなど数百万件単位で削除されるものは、テーブルとそのすべてのインデックスを肥大化させるだけです。
- 誰も復元しないデータ。 元に戻すユースケースがなければ、フィルタの負担は何ももたらしません。
deleted_at が保持期間より古い行をハードデリートします。
関連項目
- 生成列 — 一意制約の修正の背後にある仕組み
- ビュー — フィルタを忘れることのできない有効行のビュー
- 外部キー — ソフトデリートでカスケードが発火しない理由
- MySQL の DELETE — このパターンが置き換えるハードデリート