Supabaseで論理削除を実装する方法!RLSとViewを組み合わせた安全なデータ運用の手順

Supabaseで論理削除をRLSとViewで安全に運用する全体像を示したアイキャッチ データベース

Supabaseでアプリケーションを運用していると、データを物理的に削除するべきか、それとも論理削除にとどめるべきかで悩む場面は少なくありません。
特に、誤削除の防止、監査対応、将来的な復元、関連データとの整合性といった観点を考慮すると、単純なDELETE文だけでは運用上のリスクが残ります。
そのため、実務では「削除したように見せつつ、実際にはデータを保持する」論理削除の設計が重要になります。

ただし、Supabaseで論理削除を導入する場合、単にdeleted_atのようなカラムを追加するだけでは不十分です。
アプリケーション側の取得処理で毎回条件を付け忘れると、削除済みデータが意図せず表示される可能性があります。
また、RLSの設定が不十分だと、削除済みデータへの不正なアクセスや更新を完全には防げません。
つまり、論理削除は「フラグを持たせれば終わり」ではなく、データの見せ方とアクセス制御を一体で設計する必要があります。

そこで有効なのが、RLSとViewを組み合わせる方法です。
Viewによって通常利用の入口を整理し、RLSによって行レベルでアクセスを制御すれば、削除済みデータを安全に隠しながら、必要に応じて管理者だけが参照できる構成を作れます。
これにより、アプリケーションコードの複雑さを抑えつつ、データ運用の安全性を高めやすくなります。

この記事では、Supabaseで論理削除を実装する基本設計から、RLSとViewを組み合わせた具体的な手順、さらに運用時に注意したいポイントまでを順を追って整理します。
削除済みデータを安全に扱いたい方、将来の保守性まで見据えて設計したい方は、ぜひ最後まで確認してみてください。

  1. Supabaseで論理削除が必要になる理由と物理削除との違い
    1. 論理削除が向いているケース
    2. 物理削除を安易に選ぶリスク
  2. Supabaseで論理削除を実装する基本設計
    1. deleted_atカラムを使う設計の考え方
    2. deleted_byや削除理由を持たせるべき場面
  3. Supabaseで論理削除用テーブルを安全に設計するポイント
    1. 主キーや外部キーとの整合性をどう保つか
    2. インデックス設計で検索性能を落とさない方法
  4. Supabaseで論理削除を実装するSQL手順
    1. UPDATEで削除状態に切り替える実装例
    2. 復元処理を見据えたSQL設計
  5. SupabaseでViewを使って削除済みデータを隠す方法
    1. 通常利用向けViewを作成するメリット
    2. アプリ側の取得処理を単純化する考え方
  6. SupabaseのRLSで論理削除データへのアクセスを制御する方法
    1. SELECTポリシーで削除済みデータを見せない設定
    2. UPDATEとDELETEポリシーで事故を防ぐ設計
    3. 管理者だけが削除済みデータを扱える構成にする方法
  7. ViewとRLSを組み合わせたSupabaseの安全な運用パターン
    1. 一般ユーザーと管理者で参照経路を分ける設計
    2. 誤実装を防ぐための責務分離の考え方
  8. Supabaseで論理削除を運用するときの注意点と落とし穴
    1. 削除済みデータの肥大化とメンテナンス対策
    2. 集計クエリやJOINで起きやすい不整合
    3. テスト環境で確認しておきたい検証項目
  9. Supabaseで論理削除を実装するならRLSとViewの併用が有効

Supabaseで論理削除が必要になる理由と物理削除との違い

Supabaseで論理削除と物理削除の違いを整理している概念図

Supabaseでアプリケーションを構築すると、データの削除方法は単なる実装上の選択ではなく、運用設計そのものに関わる重要な論点になります。
特に、ユーザー情報、投稿データ、注文履歴、問い合わせ履歴のように、あとから参照や復元が必要になる可能性があるデータでは、物理削除と論理削除の違いを明確に理解しておく必要があります。

物理削除は、データベースから対象の行を実際に消去する方法です。
SQLでいえばDELETEを実行し、該当レコードを完全に取り除く形になります。
この方法はテーブルをすっきり保ちやすく、不要データを残さないという意味では単純です。
しかし、削除後に「やはり元に戻したい」となっても、バックアップがなければ復旧は困難です。
また、監査や履歴確認が必要な場面では、削除された事実そのものを追跡しにくくなります。

一方の論理削除は、データを消さずに「削除済み」という状態だけを記録する方法です。
たとえばdeleted_atに日時を入れる、あるいはis_deletedのようなフラグを立てる設計が典型です。
この方式では、アプリケーション上は削除されたように見せながら、実データは保持できます。
そのため、復元、監査、障害対応、誤操作の救済といった要件に対応しやすくなります。

Supabaseでは、認証、RLS、PostgreSQLのViewといった機能を組み合わせられるため、論理削除との相性が良いです。
単に削除フラグを持たせるだけでなく、削除済みデータを通常ユーザーから見えなくしつつ、管理者だけが必要に応じて確認できる構成を作りやすいからです。
つまり、Supabaseにおける論理削除は、データを残すための小手先の工夫ではなく、安全なデータ公開範囲を設計するための基盤と考えるべきです。

論理削除が向いているケース

論理削除が有効なのは、削除という操作が「完全消去」ではなく「通常画面から見えなくすること」に近い意味を持つケースです。
実務では、削除要求の背後に複数の要件が含まれていることが多く、単純な物理削除では要件を満たせない場合があります。

たとえば、次のようなケースでは論理削除が適しています。

  • ユーザーが誤ってデータを削除する可能性がある
  • 管理者が削除履歴を確認する必要がある
  • 関連テーブルとの整合性を保ちたい
  • 一定期間後に完全削除する運用を想定している
  • 法務や監査の都合で履歴保持が求められる

たとえば投稿機能のあるWebアプリでは、ユーザーが記事を削除しても、通報対応やトラブル調査のために一定期間データを保持したいことがあります。
また、ECや業務システムでは、注文や申請の履歴を利用者画面から非表示にしても、内部的には記録を残しておく必要がある場面が珍しくありません。

さらに、外部キーで複数テーブルが結び付いている場合、親レコードを物理削除すると、子レコードの扱いが問題になります。
ON DELETE CASCADEで連鎖削除する方法もありますが、必要な履歴まで消える危険があります。
論理削除であれば、参照関係を維持したまま、表示対象だけを制御できます。
この点は、データ整合性を重視する設計において大きな利点です。

物理削除を安易に選ぶリスク

物理削除は実装が簡単に見える一方で、運用フェーズに入ると多くのリスクを抱えます。
特にSupabaseのように、フロントエンドから直接データアクセスする構成を採ることがある環境では、削除操作の影響範囲を慎重に考える必要があります。

最も分かりやすいリスクは、復元不能性です。
一度DELETEされたデータは、その時点で通常の参照経路から消えます。
バックアップから戻す方法はありますが、特定の1件だけを安全に復元するのは簡単ではありません。
場合によっては、他の最新データとの整合性を崩す可能性もあります。

次に問題になるのが、監査性の低下です。
たとえば「誰が、いつ、なぜ削除したのか」を後から確認したい場合、物理削除だけでは情報が残りません。
アプリケーションログを別途保存していれば補えることもありますが、データ本体と削除履歴が分離すると、調査コストは上がりやすくなります。

また、関連データの破壊も見落としやすい論点です。
あるレコードを削除した結果、別テーブルの参照先が失われ、画面表示や集計処理で不整合が起きることがあります。
これは設計時には見えにくく、運用後に障害として顕在化しやすい種類の問題です。

物理削除を選ぶべき場面がまったくないわけではありません。
たとえば、一時データ、キャッシュ、再生成可能なログ断片など、保持価値が低く、復元要件もないデータでは合理的です。
ただし、ユーザー操作に紐づく主要データについては、まず論理削除を前提に検討し、そのうえで保持期間や完全削除の条件を設計するほうが安全です。

Supabaseでは、後続の章で扱うRLSやViewを組み合わせることで、論理削除の弱点である「削除済みデータを誤って参照してしまう問題」をかなり抑えられます。
そのため、単に削除方法を選ぶのではなく、削除後の見せ方と権限制御まで含めて設計することが、安定したデータ運用につながります。

Supabaseで論理削除を実装する基本設計

Supabaseのテーブル設計と論理削除用カラムを示す構成イメージ

Supabaseで論理削除を実装する際は、単に「削除済みかどうか」を判定するフラグを追加するだけでは不十分です。
重要なのは、削除という状態をどのようなデータとして保持し、その状態をアプリケーションやSQLからどのように扱うかを、最初に一貫したルールとして定義しておくことです。
ここが曖昧なまま実装を始めると、取得条件の書き漏れ、復元処理の不整合、監査情報の欠落といった問題が後から発生しやすくなります。

論理削除の基本設計では、少なくとも次の3点を整理しておく必要があります。

  • 削除済みであることを何のカラムで表現するか
  • 削除した主体や理由をどこまで記録するか
  • 通常の参照処理で削除済みデータをどう除外するか

Supabaseの実体はPostgreSQLであるため、設計の自由度は高いです。
しかし、自由度が高いということは、設計のばらつきがそのまま保守性の低下につながるということでもあります。
たとえば、あるテーブルではis_deletedを使い、別のテーブルではdeleted_atを使うような状態になると、クエリの書き方やRLSポリシーの条件が統一しにくくなります。
そのため、論理削除の方式はプロジェクト全体で揃えるのが基本です。

実務上は、真偽値フラグよりも日時カラムを使う設計のほうが有利な場面が多いです。
なぜなら、削除されたかどうかだけでなく、「いつ削除されたか」という時系列情報まで同時に持てるからです。
これは復元判断、監査、一定期間後の完全削除バッチなどに直結する情報です。
論理削除は単なる表示制御ではなく、運用フローの一部として設計するべきものです。

deleted_atカラムを使う設計の考え方

論理削除の代表的な実装方法が、deleted_atカラムを追加する設計です。
このカラムがNULLであれば有効なデータ、日時が入っていれば削除済みデータとみなします。
この方式の利点は、状態判定と履歴記録を1つのカラムで兼ねられる点にあります。

たとえば、テーブル定義の考え方としては次のようになります。

alter table posts
add column deleted_at timestamptz;

この設計では、削除時に対象レコードを消すのではなく、deleted_atへ削除日時を書き込みます。
すると、通常の取得処理ではdeleted_at is nullを条件に加えるだけで、未削除データだけを扱えます。
真偽値のis_deletedでも同様の判定は可能ですが、日時情報がないため、削除タイミングを別カラムで持たない限り運用情報が不足します。

また、deleted_at方式は復元処理とも相性が良いです。
復元時はこのカラムをNULLに戻せばよいため、状態遷移が明快です。
さらに、一定期間を過ぎた削除済みデータだけを物理削除するバッチ処理も書きやすくなります。
たとえば「30日以上前に削除されたデータを完全削除する」といった運用ルールを、日時比較で自然に表現できます。

一方で注意点もあります。
deleted_atを導入しただけでは、すべてのクエリが自動的に安全になるわけではありません。
開発者が通常テーブルを直接参照し、where deleted_at is nullを書き忘れれば、削除済みデータが混入します。
したがって、deleted_atはあくまで基礎データであり、実際の安全性はViewやRLSと組み合わせて担保する必要があります。
この点を理解せずに「カラムを追加したから論理削除は完成した」と考えるのは危険です。

deleted_byや削除理由を持たせるべき場面

論理削除をより実務向けに設計するなら、deleted_atだけでなく、誰が削除したのかを示すdeleted_byや、なぜ削除したのかを示すdelete_reasonのような情報も検討する価値があります。
これらはすべてのシステムで必須ではありませんが、運用要件によっては非常に重要です。

たとえば、管理画面から複数の運用担当者がデータを削除できるシステムでは、削除主体を記録しておかないと、後から原因調査が難しくなります。
ユーザー自身が退会処理の一環としてデータを削除したのか、管理者が規約違反対応として削除したのかでは、意味が大きく異なります。
deleted_byがあれば、削除操作の責任主体を追跡しやすくなります。

削除理由も同様です。
特に、問い合わせ管理、投稿監視、業務申請、社内承認フローのような領域では、削除が単なる不要化ではなく、判断を伴う業務操作になることがあります。
この場合、理由を残しておくことで、後続の担当者が文脈を理解しやすくなります。

設計判断の目安を整理すると、次のようになります。

項目 持たせるべき場面 主な目的
deleted_at ほぼ必須 削除状態と削除日時の記録
deleted_by 管理者操作や監査がある場合 削除主体の追跡
delete_reason 業務判断や審査が絡む場合 削除理由の共有と監査

ただし、情報を増やせばよいというものでもありません。
削除理由を自由入力にすると表記ゆれが起きやすく、分析や集計に向かないことがあります。
そのため、必要に応じて選択式の理由コードと補足コメントを分ける設計も有効です。
また、deleted_byに何を入れるかも明確にすべきです。
Supabase AuthのユーザーIDを入れるのか、内部管理者テーブルのIDを入れるのかで、後続の参照方法が変わります。

要するに、論理削除の基本設計は「削除済みかどうか」だけを表すものではありません。
削除というイベントを、どこまで再現可能な形で記録するかという設計です。
Supabaseでは後からRLSやViewで見せ方を制御できますが、その前提となる削除メタデータの設計が曖昧だと、運用全体が不安定になります。
まずはdeleted_atを中心に据え、必要に応じてdeleted_byや削除理由を追加する形で、要件に見合った粒度の論理削除を設計するのが堅実です。

Supabaseで論理削除用テーブルを安全に設計するポイント

Supabaseで安全な論理削除テーブル設計を整理した図

Supabaseで論理削除を導入する場合、削除フラグやdeleted_atを追加するだけでは、安全な設計になったとは言えません。
実際の運用では、主キーや外部キーとの整合性、検索性能、集計処理、アプリケーション側の参照方法まで含めて考える必要があります。
論理削除は「削除しない設計」ではありますが、データが残り続けるからこそ、物理削除よりも設計の精度が問われます。

特にSupabaseはPostgreSQLを基盤としているため、リレーショナルデータベースとしての整合性設計がそのまま重要になります。
論理削除を導入した結果、参照関係が曖昧になったり、削除済みデータがJOINに混入したり、検索速度が落ちたりすると、アプリケーション全体の品質に影響します。
そのため、論理削除は単独の機能としてではなく、テーブル設計の一部として扱うべきです。

安全な設計を考えるうえでは、少なくとも次の2点を押さえる必要があります。

  • 削除済みデータが残っても、参照整合性が崩れないこと
  • 削除済みデータが増えても、通常検索の性能が大きく落ちないこと

この2つは別々の問題に見えますが、実際には密接に関係しています。
整合性を保つためにデータを残すと、今度は検索対象の行数が増えます。
逆に、性能だけを優先して安易に物理削除すると、履歴や参照関係を失います。
したがって、論理削除の設計では、整合性と性能の両立を前提に考える必要があります。

主キーや外部キーとの整合性をどう保つか

論理削除で最初に問題になりやすいのが、主キーや外部キーとの関係です。
物理削除であれば、親レコードを消したときにON DELETE CASCADEON DELETE SET NULLといった仕組みで子レコードの扱いを制御できます。
しかし、論理削除では親レコード自体は残るため、データベース上の参照関係は維持されます。
その一方で、アプリケーション上は「削除済み」として扱うため、見た目の整合性と実データの整合性を分けて考えなければなりません。

たとえば、usersテーブルとpostsテーブルがあり、posts.user_idusers.idを参照しているとします。
このとき、ユーザーを論理削除しても、投稿データは依然としてそのユーザーIDを参照し続けます。
データベースとしては正しい状態ですが、アプリケーション側で「削除済みユーザーの投稿をどう見せるか」という判断が必要になります。
ここを曖昧にすると、一覧画面で投稿だけが残り、投稿者情報が欠けるといった不自然な表示が起きます。

この問題に対処するには、削除の単位を事前に定義しておくことが重要です。
代表的な考え方は次の通りです。

  • 親だけを論理削除し、子はそのまま残す
  • 親の論理削除に合わせて、子も論理削除する
  • 親が削除済みでも、子は独立した業務データとして残す

どれが正しいかは業務要件次第ですが、重要なのはテーブルごとに場当たり的に決めないことです。
たとえば、ユーザー退会時に投稿も非表示にしたいなら、親の削除に連動して子も論理削除する設計のほうが自然です。
一方、注文履歴のように、ユーザーが退会しても取引記録は残すべきデータであれば、子を独立して保持する設計が妥当です。

また、ユニーク制約にも注意が必要です。
論理削除では削除済みデータが残るため、たとえばメールアドレスのような一意項目で再登録を許可したい場合、単純なユニーク制約が邪魔になることがあります。
この場合は、削除済みでない行だけを対象にした部分インデックスや部分ユニーク制約を検討する必要があります。
論理削除は「行が残る」という前提を持つため、主キーや外部キーだけでなく、一意性の扱いまで再設計が必要になるわけです。

インデックス設計で検索性能を落とさない方法

論理削除を導入すると、通常の検索対象ではない削除済みデータがテーブル内に蓄積していきます。
その結果、件数が増えるほど検索性能に影響しやすくなります。
特に、アプリケーションの大半のクエリがdeleted_at is nullを条件に持つなら、その条件を前提にしたインデックス設計を行わないと、不要な行まで広く走査することになり、効率が悪くなります。

このとき有効なのが、部分インデックスの活用です。
PostgreSQLでは、特定条件を満たす行だけに対してインデックスを作成できます。
論理削除では、未削除データだけを対象にしたインデックスを作ることで、通常利用の検索性能を保ちやすくなります。
たとえば、公開中の投稿をタイトルで検索するケースが多いなら、未削除データに限定したインデックスを検討できます。

create index idx_posts_active_title
on posts (title)
where deleted_at is null;

この設計の利点は明確です。
アプリケーションの通常クエリが未削除データだけを対象にするなら、インデックス自体もその集合に絞ったほうが効率的です。
削除済みデータが大量に残っていても、通常検索の性能低下を抑えやすくなります。

ただし、インデックスは多ければよいわけではありません。
更新コストやストレージ消費も増えるため、実際の検索条件に基づいて設計する必要があります。
たとえば、次のような観点で優先順位を付けると合理的です。

  • 一覧画面で頻繁に使う絞り込み条件
  • JOINで結合に使う外部キー
  • 並び替えに使う日時カラム
  • 管理画面で削除済みデータを検索する条件

ここで見落としやすいのが、管理画面向けクエリです。
通常画面では未削除データだけを見せる一方で、管理者は削除済みデータも検索したいことがあります。
この場合、未削除向けの部分インデックスだけでは不十分なことがあります。
つまり、利用者向けと管理者向けでクエリ特性が異なるなら、それぞれに応じたインデックス戦略を考える必要があります。

また、論理削除を導入したテーブルでは、select * from table where deleted_at is nullのような条件が事実上の標準になります。
したがって、性能問題は個別クエリの最適化ではなく、設計段階での前提条件として扱うべきです。
Supabaseではアプリケーションから直接クエリされる場面も多いため、テーブル件数が増えてから対処するのでは遅いことがあります。

要するに、論理削除用テーブルを安全に設計するとは、削除済みデータを残しても整合性が崩れず、かつ通常利用の性能が落ちにくい構造を最初から作ることです。
主キーや外部キーの関係を業務要件に沿って整理し、未削除データ中心のアクセスパターンに合わせてインデックスを設計することで、Supabaseでも安定した論理削除運用がしやすくなります。

Supabaseで論理削除を実装するSQL手順

Supabaseで論理削除を実装するSQL手順を示すコード画面

Supabaseで論理削除を実装する場合、設計思想だけでなく、実際にどのSQLをどの順序で適用するかを明確にしておくことが重要です。
論理削除は物理削除の代替手段ですが、実装上は単なるDELETEの置き換えではありません。
削除状態への変更、通常取得時の除外、必要に応じた復元、さらに将来的なRLSやViewとの連携まで見据えてSQLを組み立てる必要があります。

ここで意識したいのは、論理削除を「状態遷移」として扱うことです。
つまり、レコードは存在し続けるが、利用可能な状態から削除済み状態へ移る、という考え方です。
この見方を採ると、削除処理はDELETEではなくUPDATEになります。
実務ではこの発想の切り替えが非常に重要です。
削除という言葉に引きずられて物理削除の感覚で設計すると、復元性や監査性を活かせない中途半端な実装になりやすいからです。

基本的な流れとしては、次の順序で考えると整理しやすいです。

  1. 論理削除用のカラムを用意する
  2. 削除操作をUPDATEで実装する
  3. 通常取得では未削除データだけを対象にする
  4. 復元処理を想定した更新ルールを決める
  5. 将来的にViewやRLSへ接続しやすい形に整える

この段階では、まだViewやRLSを使わず、まずはテーブル単体で論理削除が成立するSQLの骨格を作ることが目的です。
基礎となるSQLが曖昧だと、後からアクセス制御を追加しても整合性が取りにくくなります。

UPDATEで削除状態に切り替える実装例

論理削除の中核は、対象レコードを消すのではなく、削除済みであることを示す値を書き込むことです。
前の節でdeleted_atカラムを導入している前提なら、削除処理はそのカラムに現在時刻を設定するUPDATEになります。

たとえば、postsテーブルの特定レコードを論理削除するなら、考え方は次のようになります。

update posts
set deleted_at = now()
where id = '対象のID' and deleted_at is null;

このSQLで重要なのは、where deleted_at is nullを条件に含めている点です。
これにより、すでに削除済みのレコードに対して重ねて削除処理を実行することを防ぎやすくなります。
論理削除は状態遷移なので、未削除から削除済みへの一方向の変化として扱うほうが整然とします。
条件なしで更新すると、削除日時が上書きされ、最初に削除された時刻が失われる可能性があります。

また、アプリケーションから見た削除成功判定も考えておくべきです。
更新対象が0件だった場合、それは「対象IDが存在しない」のか、「すでに削除済み」なのかを区別したいことがあります。
この区別が必要なら、削除前に状態確認を行うか、更新結果の件数を見てアプリケーション側で分岐する設計が有効です。

さらに、実務では削除日時だけでなく、削除者や更新日時も同時に記録したいことがあります。
その場合、削除処理は単一カラム更新ではなく、削除イベント全体を記録する更新になります。
論理削除は単なる非表示化ではなく、業務上の操作履歴として扱うべき場面が多いためです。

通常取得のSQLも、この削除方式に合わせて統一する必要があります。
たとえば一覧取得では、未削除データだけを対象にする条件を明示します。

select id, title, created_at
from posts
where deleted_at is null
order by created_at desc;

このように、削除処理と取得処理は必ず対で考える必要があります。
削除だけ論理化しても、取得側が物理削除前提のままだと、削除済みデータが画面に出てしまいます。
論理削除のSQL手順とは、更新文だけで完結するものではなく、参照文の前提まで含めた一連の設計です。

復元処理を見据えたSQL設計

論理削除の大きな利点は、削除したデータを復元できることです。
しかし、復元を本当に安全に行いたいなら、削除時点から復元可能性を前提にSQLを設計しておく必要があります。
削除処理だけ先に作り、復元は後で考えるという進め方だと、必要な情報が足りなかったり、整合性の問題が表面化したりします。

復元の基本は単純で、deleted_atNULLに戻します。
概念的には、削除済み状態から未削除状態への逆方向の状態遷移です。
たとえば、復元対象が削除済みであることを確認したうえで更新するなら、次のような考え方になります。

update posts
set deleted_at = null
where id = '対象のID' and deleted_at is not null;

ここでも条件が重要です。
deleted_at is not nullを付けることで、未削除データに対して誤って復元処理を実行することを避けやすくなります。
削除と復元の両方で、現在の状態を条件に含めるのは、状態遷移の整合性を保つうえで基本的な考え方です。

ただし、復元は削除より難しい場合があります。
たとえば、親レコードを削除した際に子レコードも論理削除していたなら、親だけ復元して子を戻さないのか、関連データもまとめて復元するのかを決める必要があります。
また、削除後に同じ一意キーを持つ新規データが作られていた場合、復元時に制約違反が起きる可能性もあります。
つまり、復元可能であることと、無条件に安全に復元できることは別問題です。

そのため、復元を見据えたSQL設計では、少なくとも次の観点を整理しておくべきです。

  • どのテーブルが復元対象になるのか
  • 関連データを連動して復元するのか
  • 一意制約や参照整合性に衝突しないか
  • 復元を許可する権限を誰に与えるか

特にSupabaseでは、後からRLSで復元権限を制御することが多いため、SQL自体も「誰でも実行してよい更新」ではなく、「管理者や特定ユーザーだけが実行する操作」として設計するのが自然です。
復元は便利な機能ですが、誤用されると削除済みデータが意図せず再公開される危険もあります。

要するに、Supabaseで論理削除を実装するSQL手順は、UPDATEで削除状態へ切り替える処理を中心に据えつつ、その逆操作である復元まで含めて一貫した状態管理として設計することが重要です。
削除と復元の両方で現在状態を条件に含め、通常取得では未削除データだけを扱う前提を徹底することで、後続のViewやRLSとも接続しやすい堅実な実装になります。

SupabaseでViewを使って削除済みデータを隠す方法

Viewを使って削除済みデータを除外するSupabaseの構成図

Supabaseで論理削除を導入したあとに必ず問題になるのが、削除済みデータをどのように通常利用から隠すかという点です。
deleted_atカラムを追加し、削除時に日時を入れる設計までは比較的簡単ですが、その後の取得処理で毎回where deleted_at is nullを付け続ける運用は、長期的にはかなり不安定です。
開発初期は徹底できていても、画面やAPIが増えるにつれて条件の書き漏れが起きやすくなります。

この問題に対して有効なのが、SupabaseでViewを使う方法です。
Viewは、特定の条件を含んだSELECT結果を仮想テーブルのように扱える仕組みです。
論理削除と組み合わせる場合は、未削除データだけを返すViewを作っておき、通常のアプリケーション処理はそのViewを参照するようにします。
すると、削除済みデータを隠す条件を各クエリに分散させず、データベース側に集約できます。

たとえば、postsテーブルに対して通常利用向けのViewを作るなら、考え方は次のようになります。

create view active_posts as
select id, user_id, title, created_at
from posts
where deleted_at is null;

このようにしておけば、アプリケーションはpostsテーブルを直接読む代わりにactive_postsを参照するだけで、未削除データだけを扱えます。
論理削除の安全性は、削除処理そのものだけでなく、通常参照の入口をどれだけ統一できるかに大きく左右されます。
その意味で、Viewは単なる便利機能ではなく、設計上の境界を明確にするための重要な手段です。

通常利用向けViewを作成するメリット

通常利用向けのViewを作る最大の利点は、削除済みデータを除外する責務をアプリケーションコードから切り離せることです。
もし各画面や各APIで毎回deleted_at is nullを書く設計にすると、実装者ごとの癖や認識差がそのまま不整合につながります。
ある一覧画面では条件が入っていても、別の検索APIでは抜けている、といった状態は珍しくありません。

Viewを使えば、この条件をデータベース側で一元化できます。
つまり、通常利用で参照してよいデータ集合を、あらかじめ定義済みの入口として提供できるわけです。
これは保守性の観点で非常に大きな意味があります。
論理削除の条件が変わった場合でも、各アプリケーションコードを修正するのではなく、Viewの定義を見直すことで対応しやすくなります。

また、Viewは設計意図を明文化する役割も持ちます。
postsという生テーブルは削除済みデータを含む完全な記録であり、active_postsは通常公開してよいデータだけを表す、というように責務を分けられます。
これはチーム開発で特に有効です。
新しく参加した開発者でも、どのデータ源を通常利用すべきかを理解しやすくなります。

メリットを整理すると、主に次のようになります。

  • 削除条件の書き漏れを防ぎやすい
  • 取得ロジックをデータベース側に集約できる
  • アプリケーションコードの重複を減らせる
  • 通常利用用と管理用の参照経路を分けやすい
  • 設計意図を名前付きの構造として共有できる

さらに、ViewはRLSと組み合わせる前段階としても有効です。
まずViewで通常利用の見せ方を整理し、そのうえで誰がどこまで見られるかをRLSで制御する、という順序にすると、責務分離が明快になります。
表示対象の定義とアクセス権限の定義を混同しないことが、堅実な設計につながります。

アプリ側の取得処理を単純化する考え方

Viewの価値は、単に削除済みデータを隠せることだけではありません。
アプリケーション側の取得処理を単純化し、実装ミスの余地を減らせる点にもあります。
これは特にSupabaseのように、フロントエンドやサーバーサイドから比較的直接的にデータを扱う構成で重要です。

たとえば、アプリ側が生テーブルを直接参照する設計では、一覧取得、詳細取得、検索、関連データ取得のすべてで論理削除条件を意識しなければなりません。
しかも、JOINが入ると条件の付け方はさらに複雑になります。
親テーブルだけでなく、結合先にも削除済みデータが含まれる可能性があるからです。
こうした条件分岐が増えるほど、コードは読みにくくなり、レビューでも見落としが起きやすくなります。

一方、通常利用向けViewを参照する設計なら、アプリケーションは「有効なデータだけが見える入口」を使う前提で実装できます。
すると、取得処理の関心は本来の業務条件に集中できます。
たとえば、公開状態、作成日、ユーザーIDなどの絞り込みだけを考えればよくなり、論理削除の存在を毎回意識しなくて済みます。

この考え方は、責務分離として理解すると分かりやすいです。

主な責務 論理削除との関係
テーブル 完全なデータ保持 削除済みデータも含めて保持する
View 通常利用向けの公開範囲定義 未削除データだけを見せる
アプリ 業務ロジックと画面表示 Viewを前提に必要な条件だけ扱う

このように層ごとの責務を分けると、アプリケーションコードはかなり見通しが良くなります。
論理削除の条件を各所に散らさないため、将来的に仕様変更があっても影響範囲を限定しやすくなります。

ただし、Viewを導入したからといって、すべての問題が自動的に解決するわけではありません。
管理画面や監査機能では、削除済みデータを含めて参照したいことがあります。
その場合は、通常利用向けViewとは別に、管理用途の参照経路を明確に分ける必要があります。
ここを曖昧にすると、通常画面と管理画面の境界が崩れ、かえって危険です。

要するに、SupabaseでViewを使って削除済みデータを隠す方法は、単なるクエリ短縮の工夫ではありません。
論理削除を安全に運用するために、通常利用の入口をデータベース側で定義し、アプリケーションの責務を本来の業務ロジックへ集中させるための設計です。
論理削除を本当に安定運用したいなら、削除フラグの追加だけで終わらせず、Viewによって参照経路そのものを整理することが重要です。

SupabaseのRLSで論理削除データへのアクセスを制御する方法

RLSで論理削除データへの参照や更新を制御する設定イメージ

Supabaseで論理削除を安全に運用するうえで、RLSはほぼ必須の仕組みです。
Viewによって通常利用向けの参照経路を整理することはできますが、それだけでは十分ではありません。
なぜなら、アプリケーションやクライアントが誤って元テーブルへ直接アクセスした場合、削除済みデータが見えてしまう可能性があるからです。
論理削除は「消していない」設計である以上、見せないための制御を別途用意しなければなりません。
その役割を担うのがRLSです。

RLSは、行単位で参照や更新の可否を制御するPostgreSQLの機能であり、Supabaseでは認証情報と組み合わせて実用的に使えます。
論理削除との相性が良い理由は明確です。
削除済みかどうかを示すdeleted_atの状態に応じて、どの行を見せるか、どの行を更新させるかをポリシーとして定義できるからです。
つまり、論理削除の安全性をアプリケーションコードの注意力に依存させず、データベース側のルールとして固定できます。

ここで重要なのは、RLSを単なる権限制御としてではなく、データ公開範囲の最終防衛線として捉えることです。
Viewは便利ですが、参照経路の整理に近い役割です。
一方、RLSは「そもそもその行に触れてよいか」を決める仕組みです。
この違いを理解しておくと、ViewとRLSをどう分担させるべきかが見えやすくなります。

SELECTポリシーで削除済みデータを見せない設定

論理削除で最初に設定すべきなのは、削除済みデータを通常ユーザーに見せないためのSELECTポリシーです。
deleted_atが入っている行は削除済みとみなし、未削除の行だけを参照可能にするのが基本方針になります。
これにより、たとえクライアントが元テーブルを直接読もうとしても、RLSが削除済みデータを返さないようにできます。

考え方としては、未削除データだけを参照対象に限定する条件をポリシーへ埋め込みます。
たとえば、認証済みユーザーに対して未削除データのみを許可するなら、次のような構成が基本になります。

create policy select_active_posts
on posts
for select
to authenticated
using (deleted_at is null);

この設計の利点は、取得クエリの書き方に依存しないことです。
アプリケーション側で条件を書き忘れても、RLSが最後に絞り込むため、削除済みデータの露出を防ぎやすくなります。
論理削除では「データが残っていること」自体がリスクにもなり得るため、参照制御をデータベース側で強制する意味は大きいです。

ただし、ここで注意したいのは、SELECTポリシーだけで論理削除の安全性が完成するわけではないという点です。
見えないことと、更新できないことは別問題です。
削除済みデータが見えなくても、更新や再削除が可能な状態なら、運用上の事故は防げません。
そのため、SELECTは入口の制御であり、次に更新系ポリシーを整える必要があります。

UPDATEとDELETEポリシーで事故を防ぐ設計

論理削除では、物理削除を原則として避けるため、DELETE操作そのものをどう扱うかが重要になります。
もし通常ユーザーにDELETE権限を残したままだと、論理削除を採用していても、誤って物理削除される余地が残ります。
これは設計として一貫していません。
したがって、論理削除を前提にするなら、通常利用ではDELETEを禁止し、削除操作はUPDATEによる状態変更に限定するのが基本です。

まず考えるべきなのは、未削除データだけを更新可能にするUPDATEポリシーです。
これにより、削除済みデータに対する意図しない再編集を防げます。
たとえば、投稿者本人だけが自分の未削除投稿を更新できるようにするなら、所有者条件と削除状態条件を組み合わせる設計が自然です。

create policy update_own_active_posts
on posts
for update
to authenticated
using (auth.uid() = user_id and deleted_at is null)
with check (auth.uid() = user_id);

このようにしておくと、削除済みの行は更新対象から外れます。
論理削除後のデータは、通常の編集対象ではなく、別の管理対象として扱うべきだからです。

一方、DELETEポリシーについては、通常ユーザーには許可しない設計が堅実です。
論理削除を採用している以上、削除ボタンの実体はDELETE文ではなく、deleted_atを更新する処理であるべきです。
つまり、アプリケーション上の「削除」は見た目の操作名であって、データベース上の実装は更新です。
この対応関係を崩さないことが重要です。

事故を防ぐ観点では、次の方針が有効です。

  • 通常ユーザーには物理削除を許可しない
  • 更新対象は未削除データに限定する
  • 削除済みデータの再編集は管理者だけに絞る
  • 削除操作は専用の更新処理として実装する

このように設計すると、論理削除の前提とRLSの権限制御が一致します。
逆に、論理削除を採用しながらDELETE権限を広く残す設計は、運用ルールと技術的制約が食い違っており、長期的には危険です。

管理者だけが削除済みデータを扱える構成にする方法

論理削除の価値は、削除済みデータを残せることにありますが、そのデータを誰でも扱える状態にしてしまうと意味がありません。
通常ユーザーには見せず、必要な権限を持つ管理者だけが参照・復元・監査できる構成にすることが重要です。
ここでRLSは、一般ユーザー向けポリシーと管理者向けポリシーを分けることで力を発揮します。

考え方としては、通常ユーザーにはdeleted_at is nullの行だけを許可し、管理者には削除済みを含む広い範囲を許可します。
このとき、管理者判定をどのように行うかが設計上のポイントです。
Supabase Authのユーザー情報、ロール情報、あるいは別テーブルの権限管理を使って、管理者であることを識別する必要があります。

構成の整理としては、次のように考えると分かりやすいです。

利用者 参照可能なデータ 主な操作
一般ユーザー 未削除データのみ 閲覧、通常更新、論理削除
管理者 未削除データと削除済みデータ 監査、復元、特別な更新
システム管理処理 必要に応じて全件 バッチ削除、保守処理

この分離ができると、削除済みデータは通常画面から完全に隠しつつ、管理画面では必要に応じて扱えるようになります。
つまり、論理削除の「残す」と「見せない」を両立できます。

また、管理者向けには参照だけでなく、復元権限も慎重に設計すべきです。
削除済みデータを見られることと、復元できることは別の権限として考えたほうが安全です。
監査担当者は閲覧だけ、運用責任者は復元も可能、といった分離が必要な場合もあります。
RLSだけで完結しない部分はありますが、少なくとも行レベルでの基本制御はRLSで固めておくべきです。

要するに、SupabaseのRLSで論理削除データへのアクセスを制御する方法とは、未削除データだけを通常公開し、更新や削除の経路を論理削除前提に揃え、削除済みデータは管理者だけが扱えるようにすることです。
Viewが参照経路を整理する仕組みだとすれば、RLSはその背後で実際の可視範囲と操作可能範囲を保証する仕組みです。
論理削除を安全に運用したいなら、この二層構造で考えるのが最も堅実です。

ViewとRLSを組み合わせたSupabaseの安全な運用パターン

ViewとRLSを組み合わせた安全なデータ運用の全体構成図

Supabaseで論理削除を実務レベルで安定運用したいなら、ViewとRLSを別々の機能として理解するだけでは不十分です。
重要なのは、この2つをどう役割分担させ、どのような運用パターンとして組み立てるかです。
論理削除の設計では、削除済みデータを残しつつ、通常利用では見せず、必要な権限を持つ利用者だけが扱える状態を作る必要があります。
この要件を満たすうえで、ViewとRLSの併用は非常に合理的です。

まず整理すると、Viewは「どのデータ集合を通常利用の入口として見せるか」を定義する仕組みです。
一方、RLSは「その入口や元テーブルに対して、誰がどの行へアクセスできるか」を制御する仕組みです。
つまり、Viewは参照経路の整理、RLSはアクセス権限の強制という役割を持ちます。
この2つを混同すると、設計が曖昧になります。
逆に、役割を明確に分けると、アプリケーション全体の見通しがかなり良くなります。

論理削除における典型的な問題は、削除済みデータを隠す条件がアプリケーションコードに散らばることです。
さらに、管理画面では削除済みデータも見たい、一般画面では見せたくない、といった要件が加わると、単純な条件分岐では破綻しやすくなります。
そこで、通常利用向けのViewを用意し、その背後でRLSを適用する構成にすると、参照経路と権限制御を分離しながら安全性を高められます。

一般ユーザーと管理者で参照経路を分ける設計

論理削除を安全に運用するうえで、一般ユーザーと管理者が同じ参照経路を使う設計は避けたほうがよいです。
なぜなら、両者は必要とするデータ範囲が根本的に異なるからです。
一般ユーザーには未削除データだけを見せれば十分ですが、管理者は削除済みデータを含めて確認したいことがあります。
この違いをアプリケーション側の条件分岐だけで吸収しようとすると、実装が複雑になり、誤表示や権限漏れの原因になります。

そのため、参照経路そのものを分ける設計が有効です。
たとえば、一般ユーザー向けには未削除データだけを返すViewを使い、管理者向けには元テーブル、あるいは管理専用Viewを使う構成が考えられます。
こうすると、一般画面の実装者は「通常利用向けの入口だけを使う」というルールに従えばよく、削除済みデータの存在を毎回意識しなくて済みます。

構成の考え方を整理すると、次のようになります。

  • 一般ユーザーは未削除データ専用のViewを参照する
  • 管理者は削除済みを含む管理用の参照経路を使う
  • RLSで各経路に対する実際の可視範囲を制御する
  • 元テーブルは原則として通常画面から直接触らせない

この設計の利点は、データの見せ方と利用者の役割が自然に対応することです。
一般ユーザー向け画面で削除済みデータが混入する事故は、参照経路の時点でかなり防ぎやすくなります。
一方、管理者向け画面では、削除済みデータを含む前提で検索、監査、復元といった操作を設計できます。

また、この分離はセキュリティだけでなく、保守性にも効きます。
一般向け機能の改修時に管理画面の要件を意識しすぎる必要がなくなり、逆に管理機能の拡張時も通常画面への影響を抑えやすくなります。
論理削除は単なる削除方式ではなく、利用者ごとのデータ公開範囲を設計する問題でもあるため、参照経路の分離は非常に本質的です。

誤実装を防ぐための責務分離の考え方

ViewとRLSを組み合わせる最大の価値は、誤実装を個人の注意力ではなく、構造で防げることにあります。
論理削除の失敗は、多くの場合、技術的に難しい処理が原因ではありません。
むしろ、単純な条件の書き忘れ、参照先の選択ミス、権限設定の抜け漏れといった、人間の実装ミスによって起こります。
したがって、安全な運用を目指すなら、ミスしにくい構造を先に作るべきです。

ここで重要になるのが責務分離です。
どの層が何を担当するのかを明確にしておくと、実装の判断基準がぶれにくくなります。
論理削除における責務分離は、概ね次のように整理できます。

主な責務 具体例
テーブル 完全なデータ保持 削除済みデータも含めて保存する
View 通常利用向けの公開範囲定義 未削除データだけを返す
RLS 利用者ごとのアクセス制御 一般ユーザーと管理者の可視範囲を分ける
アプリ 業務ロジックと画面制御 どの画面でどの参照経路を使うか決める

このように分けておくと、たとえば「削除済みデータが見えてしまった」という問題が起きたときも、どの層の責務が破れたのかを切り分けやすくなります。
View定義の問題なのか、RLSポリシーの問題なのか、あるいはアプリが誤った参照経路を使っているのかを論理的に追えます。
これは障害対応の速さにも直結します。

さらに、責務分離はチーム開発で特に有効です。
フロントエンド担当、バックエンド担当、インフラ寄りの担当が混在する環境では、全員が同じ粒度で論理削除を理解しているとは限りません。
そのため、「通常画面はこのViewだけを使う」「削除済みデータの可視範囲はRLSで保証する」といったルールを構造として固定しておくことが重要です。
ルールがコードや設定に埋め込まれていれば、口頭の共有に頼る必要が減ります。

一方で、責務分離が不十分だと、アプリ側で削除条件を足したり外したりする実装が増え、ViewやRLSの存在意義が薄れます。
これは短期的には柔軟に見えても、長期的には一貫性を失います。
論理削除のように複数の画面やAPIにまたがる関心事は、できるだけ下位層で共通化し、上位層ではその前提を利用する形にしたほうが堅実です。

要するに、ViewとRLSを組み合わせたSupabaseの安全な運用パターンとは、一般ユーザーと管理者で参照経路を分け、データの見せ方はView、アクセス権限はRLS、業務ロジックはアプリという形で責務を分離することです。
この構造を取ることで、論理削除の安全性は個別実装の丁寧さではなく、システム全体の設計によって支えられるようになります。
結果として、削除済みデータを残しながらも、通常利用では自然に隠し、必要な場面だけ適切に扱える運用が実現しやすくなります。

Supabaseで論理削除を運用するときの注意点と落とし穴

Supabaseの論理削除運用で起こりやすい注意点を示す図

Supabaseで論理削除を実装できたとしても、それで運用上の課題がなくなるわけではありません。
むしろ本当に重要なのは、実装後にどのような問題が起こり得るかを理解し、あらかじめ対策を組み込んでおくことです。
論理削除は、物理削除に比べて復元性や監査性に優れていますが、その代わりに「削除済みデータが残り続ける」という性質を持ちます。
この性質が、性能、整合性、保守性の面で新たな論点を生みます。

特にSupabaseでは、フロントエンドから直接データを扱う構成や、RLS・Viewを組み合わせた設計を採ることが多いため、論理削除の影響が複数の層にまたがります。
つまり、単にSQLが正しく動くかだけでは不十分で、一覧表示、集計、管理画面、権限制御、テスト運用まで含めて考える必要があります。
論理削除は便利な仕組みですが、設計時に見えていなかった問題が運用フェーズで表面化しやすい点には注意が必要です。

ここでは、特に見落とされやすい3つの論点として、削除済みデータの肥大化、集計やJOINでの不整合、そしてテスト環境での検証項目を整理します。
これらを事前に押さえておくと、論理削除を導入したあとに「想定より扱いが難しい」と感じる場面をかなり減らせます。

削除済みデータの肥大化とメンテナンス対策

論理削除の最も分かりやすい副作用は、削除済みデータがテーブル内に蓄積し続けることです。
物理削除であれば不要な行は消えますが、論理削除では残ります。
そのため、運用期間が長くなるほど、実際に使われている未削除データよりも、削除済みデータのほうが多くなるケースもあります。
これはストレージ消費だけでなく、検索性能や保守作業にも影響します。

特に問題になりやすいのは、通常利用では不要なデータが大量に残ることで、インデックス効率やクエリ計画に悪影響が出ることです。
部分インデックスを適切に設計していればある程度は抑えられますが、それでもテーブル全体の肥大化は無視できません。
また、バックアップやデータ移行の対象にも削除済みデータが含まれるため、運用コスト全体がじわじわ増えていきます。

この問題に対しては、論理削除を導入する時点で保持期間の方針を決めておくことが重要です。
たとえば、削除後30日や90日を過ぎたデータは、監査要件に問題がなければ物理削除する、といったルールを設ける方法があります。
論理削除は「永久保存」を意味するものではなく、「一定期間は残す」という運用と組み合わせるほうが現実的です。

対策の方向性としては、次のようなものがあります。

  • 削除済みデータの保持期間を定義する
  • 定期的な物理削除バッチを用意する
  • 管理画面で削除済みデータ件数を把握できるようにする
  • 大規模テーブルではアーカイブ戦略も検討する

重要なのは、論理削除を導入した時点でメンテナンス責務も発生するという認識です。
削除済みデータを残すこと自体は簡単ですが、その後どう整理するかを決めていないと、将来的に性能問題や運用負荷として跳ね返ってきます。

集計クエリやJOINで起きやすい不整合

論理削除で実務上もっとも厄介なのは、一覧表示よりもむしろ集計クエリやJOINで起きる不整合です。
単純な一覧取得であればdeleted_at is nullを付けるだけで済むことが多いですが、複数テーブルを結合したり、件数や合計値を計算したりする場面では、削除済みデータの混入が見えにくくなります。

たとえば、投稿数をユーザーごとに集計する処理を考えると、投稿テーブル側で削除済みデータを除外していなければ、画面上では見えない投稿まで件数に含まれる可能性があります。
また、親テーブルは未削除でも、JOIN先の子テーブルに削除済みデータが混ざっていると、集計結果や表示内容が実態とずれることがあります。
これは一見すると小さな差に見えても、レポートや管理指標では重大な誤差になります。

JOINで特に注意したいのは、どのテーブルの削除状態を基準に結果を構成するかです。
親が削除済みなら子も見せないのか、子だけ削除済みなら親は残すのか、といったルールを明確にしないと、クエリごとに解釈がぶれます。
論理削除はデータが残るため、物理削除よりも「何を有効データとみなすか」の定義が重要になります。

この種の不整合を防ぐには、次の観点でクエリ設計を統一するとよいです。

  • 集計対象に削除済みデータを含めるかを明示する
  • JOIN対象の各テーブルで未削除条件を確認する
  • 通常利用向けの集計はView経由で行う
  • 管理用集計と一般向け集計を分ける

つまり、論理削除ではクエリの意味論が変わります。
単に行が存在するかではなく、その行が現在の業務上有効かどうかを毎回意識しなければなりません。
ここを曖昧にすると、画面表示と集計結果が一致しない、管理画面の件数とAPIレスポンスが合わない、といった問題が起こります。

テスト環境で確認しておきたい検証項目

論理削除は、実装そのものよりも、境界条件での挙動確認が重要です。
そのため、テスト環境では通常のCRUD確認だけでなく、削除済み状態を前提にした検証項目を意識的に用意する必要があります。
ここを省くと、本番で初めて削除済みデータの混入や権限漏れに気づくことがあります。

最低限確認しておきたいのは、削除、参照、更新、復元の各操作が想定通りに分離されているかです。
たとえば、一般ユーザーが削除済みデータを参照できないか、削除済みデータを通常更新できないか、管理者だけが復元できるか、といった点は必ず検証すべきです。
RLSやViewを使っている場合は、SQL単体の正しさだけでなく、実際の認証状態ごとの挙動を確認する必要があります。

確認項目を整理すると、次のようになります。

検証項目 確認内容 目的
通常一覧取得 削除済みデータが表示されないか 一般画面の安全性確認
詳細取得 直接ID指定でも削除済みが見えないか 参照漏れ防止
更新処理 削除済みデータを通常更新できないか 状態整合性の確認
復元処理 権限のある利用者だけが復元できるか 管理機能の安全性確認
集計処理 件数や合計値に削除済みが混入しないか レポート整合性の確認

さらに、削除済みデータが存在する状態でのJOIN、検索、ページネーションも確認しておくべきです。
論理削除はデータ件数や並び順にも影響するため、単純な表示確認だけでは不十分です。
特にページネーションでは、削除済みデータを除外した結果として件数計算がずれないかを見ておく必要があります。

要するに、Supabaseで論理削除を運用するときの注意点は、実装後に残るデータをどう扱い続けるかに集約されます。
削除済みデータの肥大化を放置しないこと、集計やJOINで有効データの定義をぶらさないこと、そしてRLSやViewを含めた実運用の挙動をテスト環境で丁寧に確認することが重要です。
論理削除は便利ですが、運用設計まで含めて初めて安全に機能する仕組みだと理解しておくべきです。

Supabaseで論理削除を実装するならRLSとViewの併用が有効

SupabaseでRLSとViewを併用した論理削除の要点をまとめたイメージ

Supabaseで論理削除を実装するなら、結論としてはRLSとViewを併用する構成が最も実務向きです。
deleted_atのようなカラムを追加して削除状態を保持するだけでも、論理削除そのものは成立します。
しかし、それだけでは安全な運用にはなりません。
なぜなら、論理削除は「データを残す」設計である以上、残したデータを誰にどう見せるか、どこまで操作させるかを別途きちんと制御しなければならないからです。

この点で、ViewとRLSは役割が明確に異なります。
Viewは通常利用で見せるべきデータ集合を定義する仕組みであり、RLSはそのデータに対して誰がアクセスできるかを制御する仕組みです。
つまり、Viewは参照経路の整理、RLSは権限の強制という役割を持ちます。
この2つを組み合わせることで、論理削除にありがちな「削除済みデータの参照漏れ」と「権限設定の抜け漏れ」を同時に抑えやすくなります。

たとえば、postsテーブルに削除済みデータが残っているとします。
このとき、通常のアプリケーション画面では未削除データだけを見せたい一方で、管理画面では削除済みデータも確認したいことがあります。
もしこれをアプリケーションコードだけで制御しようとすると、画面ごと、APIごと、クエリごとに条件分岐が増えます。
その結果、ある画面ではdeleted_at is nullが入っているのに、別の画面では抜けている、といった不整合が起こりやすくなります。

Viewを使えば、この通常利用向けの条件をデータベース側に集約できます。
たとえば未削除データだけを返すViewを用意しておけば、一般的な一覧取得や詳細取得はそのViewを参照するだけで済みます。
これにより、アプリケーション側は論理削除の存在を毎回意識せずに済み、取得処理をかなり単純化できます。
つまり、Viewは「安全な既定値」を作るための仕組みとして機能します。

一方で、Viewだけでは十分ではありません。
なぜなら、元テーブルへの直接アクセス経路が残っている限り、削除済みデータが見える可能性があるからです。
ここでRLSが必要になります。
RLSを設定しておけば、たとえクライアントやアプリケーションが誤って元テーブルを参照しても、削除済みデータを返さないようにできます。
また、更新や削除の権限も行単位で制御できるため、論理削除を前提とした安全な操作制限を実現しやすくなります。

この併用構成の利点は、責務分離が明快になることです。
整理すると、次のような分担になります。

要素 主な役割 論理削除での意味
テーブル 完全なデータ保持 削除済みデータも含めて保存する
View 通常利用向けの公開範囲定義 未削除データだけを見せる
RLS 利用者ごとのアクセス制御 誰がどの行を見たり更新したりできるか決める
アプリ 業務ロジックと画面制御 適切な参照経路を使って機能を実装する

この構造の強みは、実装ミスを個人の注意力に依存しにくくなる点です。
論理削除の失敗は、高度なアルゴリズムの問題ではなく、条件の書き忘れや参照先の選択ミスで起こることが多いです。
したがって、ミスしにくい構造を先に作ることが重要です。
Viewで通常利用の入口を固定し、RLSで最終的な可視範囲を強制する構成は、その意味で非常に合理的です。

また、この併用は一般ユーザーと管理者の役割分離にも向いています。
一般ユーザーには未削除データだけを見せ、管理者には削除済みデータを含めた参照や復元を許可する、といった構成を作りやすくなります。
ここで重要なのは、一般向けと管理向けで参照経路を分けることです。
一般画面は通常利用向けViewを使い、管理画面は管理用Viewまたは元テーブルを使う、といった設計にすると、画面ごとの責務が明確になります。
そのうえでRLSを適用すれば、仮に誤った経路を使っても、権限面での防御が働きます。

さらに、保守性の面でもこの構成は有利です。
将来的に「削除済みの定義」を変えたくなった場合、たとえばdeleted_atだけでなく公開状態や凍結状態も考慮したい場合でも、View定義やRLSポリシーを見直すことで対応しやすくなります。
もし条件がアプリケーション全体に散らばっていれば、修正漏れのリスクが高くなります。
論理削除は一度導入したら終わりではなく、運用要件に応じて見直される可能性があるため、変更しやすい構造にしておくことが重要です。

もちろん、RLSとViewを併用すれば何も考えなくてよいわけではありません。
管理画面でどこまで削除済みデータを見せるのか、復元権限を誰に与えるのか、一定期間後に物理削除するのか、といった運用ルールは別途必要です。
ただし、それらのルールを実装へ落とし込む土台としては、RLSとViewの併用が非常に扱いやすいです。
論理削除の本質は、削除済みデータを残すことではなく、残したデータを安全に制御することにあります。

要するに、Supabaseで論理削除を実装するなら、Viewで通常利用の参照経路を整理し、RLSでアクセス可能な行を厳密に制御する構成が有効です。
Viewだけでは権限面が弱く、RLSだけでは参照経路が煩雑になりやすいです。
この2つを併用することで、見せ方と権限制御を分離しながら、削除済みデータを安全に扱える設計になります。
実務で安定したデータ運用を目指すなら、論理削除は単独のテクニックとしてではなく、RLSとViewを含めた全体設計として捉えるべきです。

コメント

タイトルとURLをコピーしました