Web系エンジニアのアウトプット練習場

エンジニアリングと書評が中心。たまに全然関係無い話もします。

マイクロサービスアーキテクチャにおける論理削除と物理削除の選択について考えてみる

マイクロサービスアーキテクチャのイメージ

論理削除とは

「実際に記憶装置からデータを削除するのではなく、削除フラグを立てることで削除されたとみなすようにすること(対義語: 物理削除)」です。

例えば、リレーショナルデータベースでの論理削除の実装例としては、

  • timestamp型のdeleted_atというカラムを用意し、deleted_atnullでなければ、削除されたとみなす
  • deletedというboolean型のカラムだけ用意し、deletedtrueであれば、削除されたとみなす

というような方法が考えられます。

「削除されたとみなす」には、データを全件取得したい場合でも、以下の例ように削除フラグをチェックする必要があります。

  • select * from items where deleted_at is not null;
  • select * from items where deleted = false;

上記のように生のSQLではなく、論理削除をサポートしているORM*1やライブラリを使用するという手も考えられます))やライブラリを使用するという手も考えられます。

例えば、Ruby on Railsに付属しているActive Recordには論理削除のサポート機能は特にありませんが、paranoiaというgemと組み合わせると、論理削除を簡単に実装することができます。
paranoiaを導入したActive Recordを継承したクラスは明示的にunscopedしない限り、勝手にdeleted_atカラムのチェックを行ってくれるようになります。

# paranoia導入前の全件取得
Item.where.not(deleted_at: nil)
# => select * from items where deleted_at is not nil;

# paranoia導入後の全件取得
Item.all
# => select * from items where deleted_at is not nil;
# 論理削除済データも含めた全件取得
Item.unscoped
# => select * from items;

ここまで、簡単に実装できるのであれば、どうして多くのフレームワーク(付属するORM)がデフォルトの挙動として論理削除を採用していないのでしょうか。
それはやはりデメリットもあるからです。

次は、論理削除のデメリットって何やねんっていうのを整理したいと思います。

論理削除のメリット・デメリット

メリット

先程、論理削除は「実際にデータを削除するわけではなく、削除フラグを操作して論理的に削除されているとみなすことにする」と説明しました。
この特徴によって得られるメリットが何なのかはおそらく想像に難くないと思います。

  • 削除したデータをいつでも復元することができる
  • データを削除しても他のリソースからの参照先としてアクセスできる

といったところです。

削除したデータをいつでも復元することができる

削除フラグを下ろすだけで、簡単に削除前の状態に戻すことができます。

物理削除の場合も、直前のバックアップとログ等からデータを復元できますが、圧倒的に復元コストが高いです。

データを削除しても他のリソースからの参照先としてアクセスできる

productsテーブルと、商品に対するレビューを保存するreviewsテーブルがあったとします。
reviewsは、レビューがどの商品に対するものなのかを表現するために、product_idカラムを持つでしょう。
ここで、一部の商品に何か欠陥が見つかって商品一覧からは削除しなければならないが、せっかくユーザーから頂いたレビューは残しておきたい、という状況になったとします。

物理削除で実装していた場合、商品テーブルから該当商品を削除してしまうと、参照が残っていても実際のデータが消えているため、参照エラーとなってしまいます。
そのため、外部キー制約を設定することによって、その商品を参照しているレビューも削除されるようにする、もしくは、参照先の商品の削除と同時にレビューのproduct_idnullが設定する、といった挙動にすることが多いでしょう。

一方、論理削除であれば、商品を削除した場合に、レビューのproduct_idをそのままにしておいても、レビューから商品を参照する際にwhere句から削除フラグのチェックさえしなければ、商品情報をそのまま取得することができます。

では、デメリットはなんでしょうか。

デメリット

ざっくり言うとこんな感じです。

  • 削除フラグのチェックを忘れることによるバグを埋め込みやすい
  • ユニーク制約を設定するときに削除フラグの考慮が必要
  • データ数の増大に伴う検索パフォーマンス劣化

削除フラグのチェックを忘れることによるバグを埋め込みやすい

完全な削除が期待される場面で、削除フラグのチェック忘れにより痛い目を見ることがあります。

例えば、提供しているサービスからユーザーが個人情報を削除したい場合などでも、他のリソースからの参照により情報を復元できてしまう可能性があります。
サービスが大きくなればなるほど、ユーザーを参照しているリソースも増えますが、そのどれか1つでも削除フラグのチェックを忘れていれば、ユーザーの情報が公開されたままになってしまいます。

冒頭で紹介したActive Recordにおけるparanoiaのようなライブラリがあれば、ある程度このデメリットは回避することができます。

ユニーク制約を設定するときに削除フラグの考慮が必要

例として、ユーザーテーブルのuser_idにユニーク制約を設定する場合を考えます。

user_id name deleted_at
h-sakano Hiroki Sakano 2019-12-31 00:00:00
h-sakano Hiroshi Sakano null

1レコード目のh-sakanoさんは2019年の大晦日にユーザーデータを削除済です。
そのため、2レコード目のh-sakanoさんは問題なく登録できることが期待されます。

しかし、論理削除の場合、削除したレコードがそのままDBに残っているため、単純にuser_idにユニーク制約を設定するだけでは、2レコード目のh-sakanoさんは登録できなくなってしまいます。
そのため、user_idにユニーク制約を設定したければ、deleted_atも含めた複合キーに対してユニーク制約を設定する必要があります。

データ数が増大に伴う検索パフォーマンス劣化

これは論理削除の構造上避けては通れないデメリットです。

物理削除では、削除することで実際に検索対象のレコードが減りますし、削除フラグのチェックをする必要もないので、当然物理削除の方が論理削除よりも検索パフォーマンスは高くなります。
特に頻繁に削除されるようなリソースの場合は差が顕著になります。

マイクロサービスアーキテクチャと論理削除

ここまで、論理削除のメリットとデメリットについて整理しました。

ではようやく本題ですが、マイクロサービスアーキテクチャにおいてDBを設計する際に、物理削除と論理削除、どちらを採用すれば良いのでしょうか。

僕の考えはこうです。

各マイクロサービスの性質に合った方法をそれぞれ検討すべきだが、基本的には論理削除を採用したほうがメリットが大きい

1つ1つのサービスの性質に合った方法をそれぞれ検討すべき

マイクロサービスアーキテクチャでサービスを構築するメリットの1つに、「各マイクロサービスに合った環境*2を選択できる」ということがあると思います。

DB設計においてもそれは同じで、

  • ユーザー情報を取り扱うマイクロサービスにおいては、削除後の個人情報に関しては、他のサービスから絶対に参照されないように物理削除を選択する
  • ECシステムの受注サービスが保存する受注リソースは他のサービスからも多く参照されるし、誤って削除した場合に復元できるようにしたいので論理削除を採用する

といったように、サービスごとに物理削除と論理削除を選択することができます。

これはマイクロサービスアーキテクチャを採用した以上、ある種当たり前という感じもします。

基本的には論理削除を採用したほうがメリットが大きい

ただし、ユーザー情報のようにセンシティブな内容であるなどの特別な理由が無い限り、論理削除を選択したほうがメリットが大きいと考えています。

というのも、マイクロサービスアーキテクチャの場合、サービスを跨いだリソース参照に対して外部キー制約が設定できないからです。*3

例えば、ECシステムをマイクロサービスアーキテクチャで開発するにおいて、受注サービスの受注リソースは、発注サービス、出荷サービスなど多くのサービスから参照されることが予想できます。

モノリシックなECシステムであれば、受注リソース、発注リソース、出荷サービスが全て同じDB内にあるため、

  • 1件以上参照されている場合は削除できないようにする
  • 削除するリソースを参照しているリソースも同時に削除する
  • 削除するリソースを参照している場合は、その参照を解除する

といった方針が、DBの外部キー制約やORMの機能によって簡単に実現できます。

一方、マイクロサービスアーキテクチャの場合は、上記の方針のいずれかを採用しようとも、参照される可能性があるサービスをAPI経由でチェックし、個別に処理しなければなりません。
サービスが成長し、あるリソースを参照するサービスが増えるたびに、この処理に追加していかなくてはならなくなり、サービスの成長を大きく阻害する癌になってしまうでしょう。
マイクロサービスアーキテクチャによって提供するサービスがどの程度成長し、どの程度の数のマイクロサービスから参照されるかを開発前に見積もることはほぼ不可能です。

以上の理由から、

各マイクロサービスの性質に合った方法をそれぞれ検討すべきだが、基本的には論理削除を採用したほうがメリットが大きい

という結論に至りました。

まとめ

  • マイクロサービスでは論理削除の方がメリットが大きい場合が多い
  • ただし、論理削除は一長一短あるので、採用する際はデメリットを許容できるかをしっかり検討する

以上です!

マイクロサービスアーキテクチャに関する情報はまだまだ少なくて、私も本当に手探りの状態です。
ご意見がございましたら、このブログや@h_sakanoまでコメントいただければ嬉しいです!

よろしくお願いしますm(__)m

*1:ORM - Wikipedia

*2:実行環境、フレームワーク、言語など

*3:マイクロサービスアーキテクチャを採用しながら、各サービスが1つのDBを共有するパターンはアンチパターンとして有名です