仕事で、削除フラグのカラムを使ってアカウントの無効化を実装しようとしているPRがあり、削除フラグを使うよりも、状態遷移をモデリングするか履歴テーブルを設けることを検討したほうが良いという指摘がありました。
そして、こちらのpodcastを教えてもらったので聞いてみたら、とてもためになる話でした。
fukabori.fm:Apple Podcast内の27. 論理削除とは何か?どのような解法があるのか? w/ twada
話を聞いて、「とりあえず削除フラグを設ける」という設計はアンチパターンだということがとても納得できました。 (SQLアンチパターンの幻の第26章なんて話も出てました)
ブログに内容のメモを残したいと思います。
fukabori.fmのpodcast、他にも気になる話がいろいろありそうです。
まとめ
- 論理削除や削除フラグは悪いわけではないが、とりあえず削除フラグを設けるのは思考停止でアンチパターン
- 論理削除という言葉が出ていた時点で設計不足を示してる
- 実装として、フラグ以外もある
- 状態遷移
- アーカイブテーブル
- そもそもdbの設計の流派によっては企業内システムにおいてはそもそも事実を失わないために更新や削除をしないというdb設計の世界もある
- それらもろもろ考えた上で、削除フラグで今回はいいかというのであればそれはそれでよい
「とりあえず削除フラグ」はアンチパターン
- デメリットは、where句にかならず削除フラグが必要になってしまう
- テーブルをjoinするとすべて削除フラグが必要になってしまう、可読性も悪い
- プロジェクトの共通ライブラリに、デフォルトで、is_deletedはfalseであると入るようにする
- それを理解してコードを書かなければいけなくなる
- 罪が重い設計
- 削除フラグを含めてユニークインデックスをいれなければいけないことが多い
- 削除フラグを含めたユニークインデックスを入れていないと、削除フラグがon/offのレコードが2件引っかかってしまう
- whereにおさまらずに場当たり的なクエリ(distinctなど)がプロジェクト全体に散らばる
- 全部のテーブルにis_deletedが入ってる、プロジェクトのdb規約に全てのテーブルに削除フラグを入れていることになってる、などはこのアンチパターンのにおい
- 盲目的に削除フラグを入れてるのがこのアンチパターン
満たしたい要件は妥当
- ユーザからは見えないようにしたいけどレコードは消したくない
- 削除済みのレコードを検索したい
- 削除した誤操作をすぐ元に戻せるようにしたい など
実装として削除フラグを使うか別の方法を使うか
- 認可でフラグがあったりすると、複数のフラグで見せるデータを判断するとき、フラグだらけになる
- やりたいことはステートの判断
- ステートを表すカラムがあれば十分では?
どうやって解決するか?
- 論理削除をどうやって実装するか
- どうやって問題として捉えるか
論理削除ということばはなにか?
論理削除はシステムやが使う言葉
- 論理削除という言葉が出てきた時点で、深堀がたりてないと考えるのが第一歩
- 実装の前段に考えを深めるための要求ヒアリングをするべき。
論理削除というもので実装したいものを聞いていくと
- 社内システムつくるときの社員の退職
- システムからそのユーザを消す
- ユーザが関わった過去のデータや社員が扱っていたデータが消えてしまうのは避けたい
- 論理削除ではなく退職なのでちゃんとモデリングする
- 退職した後、復職もあるかもしれない、状態遷移かもしれない
- 議論を深めて設計していくほうがよい
- 非表示の公開前というステータスを削除フラグで表すことも多い
- 状態遷移で表すほうがずっといい
- システムには可視性の制御をすることがある
- 状態遷移や認可に踏み込むことなしにとりあえず全部のテーブルに削除フラグといのはアンチパターン
アンチパターンに陥らないための第一歩
- 論理削除という概念は世の中にない
- お客さんとも論理削除という要件の話はしない
- 退職、非表示、公開前、既読未読といった言葉を捉えて設計に反映させましょう
- ドメインの言葉として論理削除はない
どうやって概念を設計に落としていくか
orマッパーの論理削除プラグイン
- 実装して逆算して考えると、orマッパーに論理削除プラグインがあったりする
- それもアンチパターンの入り口になる
- orマッパー論理削除プラグインは、だいたい、日付のカラムを1個導入する
- booleanプラグよりましである程度の話
- nullを許すかどうかでひともめある
- nullだったら削除されてないとみなし、not nullであれば日付を判定するという話になりつらい
- インデックスをうまく活用できるかというと、boolのほうがうまく活用できるという皮肉な話もある
- 削除日を入れるソリューションも、フラグよりはよいが、要件をよく考えて、もう一歩ふみこんで設計しよう
1. 状態遷移のモデリング
- 状態として扱おう、というのが設計選択方法になる
- ステータス、状態遷移であると捉えよう
- 最近のorマッパーのいくつかは、soft deleteプラグインがデフォルトで備わってるのがけっこうあったが、やめるorマッパーが出てきてる
- ステートパターンが使えないケースもある?
- そんなにない
- モデリング対象が状態遷移を扱うなら第一の選択肢になる
- 企業ブログはドラフトから審査依頼すると審査待ち、審査中、2人の許可が出たら公開状態になる、公開中止にする、一定期間したら公開終了済みに遷移する、却下
- 状態遷移のモデリング
- 各状態の名前をステータスカラムに入れて行こうというのが状態遷移のモデリング
- 状態遷移とかステートマシンという言葉をコンピュータサイエンスを学んだ人なら、状態遷移のステートマシンというものは、一般的な用途のライブラリとしてある
- おすすめなのは、論理削除でなくステートマシンのプラグインをさがすこと
- 自然に筋の良い設計になる
- ステートマシンが何をするか
- 基本的には、状態遷移を宣言的に書けるようになるのがメリットその1
- メリットその2が不正な状態遷移を防いでくれる
- 状態遷移は向きが決まっていて、遷移を飛ばすことはできない
- ブログ記事だと、ドラフトから突然公開中にできない
- なんらかのバグによって公開中になるというバグを防ぐには、状態遷移をすべてステートマシンを通すことによって、ステータスがdraftからpublishedにいこうとしたら不正な遷移なので例外が出る
- そもそも間違いが起こらない設計を持ち込むことができる
- ステートマシン系のプラグインを導入するのがおすすめ
railsのプラグイン aasm
- t-wadaさんはサーバーサイドのシステムはrailsで作ることが多い
- aasm(acts as state machineの略)というプラグインがある
- そのプラグインを使うと、このモデル、たとえばブログのentryモデルだとしたら、モデルに、状態はこんなものがあって、transitionにブログの初期状態から審査待ち状態にはこのメソッドで遷移するというのを全部宣言的に書ける
- dbの中のステータスの値をdistinctして取らないと状態にどんなものがあるかわからないのは本末転倒
- そうでなく、コード読むだけで、どんな状態があり、どう状態遷移するのか読めるのがありがたい
- カラム見てそのなかの遷移を調べるのは時間かかる
状態遷移まとめ
- よくあるソリューションでおすすめなのは、状態遷移でモデリングしなおす
- 実装レベルでいったら、ステートマシンを使い、不正な状態を起こさないようにし、状態遷移を宣言的に書けるようになり、
- リレーショナルdbであればインデックスをステータスカラムをvarcharで宣言したり、最近ではenumもある
- enum型やvarchar型を使って状態を表していくのが基本的に第一の候補としておすすめ
2.履歴テーブル
- もうひとつの対論理削除の設計案は、履歴テーブル
- 第一の要件、状態遷移を論理削除で表してる場合は状態遷移をモデリングしてステートマシンを使うのがおすすめ
- ユーザからは削除されてるように見えるが削除したくない、またはログとしてリレーショナルdb上に残しておきたい要件の場合、第二のソリューションとしておすすめできるのが履歴テーブルにうつす
- たとえば、現役のデータが入ってるテーブル、削除済みのデータが入ってるテーブルの対のテーブルをつくる
- ステートパターンでもできる
- deletedみたいなステートにしてもいい
この設計にするモチベーション
- 2つのテーブルに分けて持ちたくなる強いモチベーションは?
- たぶん、データの比率
- データの特性にもよるが、削除フラグでシステムを長く運用すると、現役データより削除済みデータの方がはるかに多いという状態があったりする
- 現役のデータを検索するために削除済みデータが多すぎて、where句で削除フラグを検索しつつ第二のほんとの使いたいインデックスのカラムを第二インデックスのふたつめのカラムにするという検索しなきゃいけない
- rdbのパワーを使い切りたいのに、削除フラグやステータスカラムが邪魔で本当に使いたい検索ができないパターンが挙げられる
- rdbはベンダーによっては、テーブルごとにインデックスを1回のクエリにインデックスをひとつしか使えない制約がある
- 各テーブルにインデックスが使えない貴重なインデックスに削除フラグやステータスが入ってくるのがつらい
- 見せたいデータと、とっておきたいけど見せたいわけでないデータの比率が著しく偏ってるパターンはアーカイブテーブルを使いたいシチュエーションのひとつ
3.削除も更新もしない
- 第3のソリューションは、そもそも削除も更新もしない
- web系ではなくエンタープライズ系のシステムの知見
そもそもなにかを削除するのは現実世界の要件としてあるかというとあまりない
- 社員の退職は、退職したという情報を既存のものに加えていくので、存在していた社員とそれに紐作りデータを消すということではない
- これまで状態遷移であれば、ステータスカラムにのupdateをベースとしたソリューションの話をした
- 状態遷移したのであれば新たな状態を加えるということにより、過去の状態も残り、最新の状態も加えられる
- エンプラ、業務システムの世界では、企業が企業活動において発生した事実を消さずに漏らさず格納していくのがすごく大事な要件になる
- 発生した事実に忠実にモデリングしていったらそもそも情報やデータをdeleteするupdateするのはやってはいけないこと
- つまり改竄
- 企業活動をもらさず格納することが求められるエンタープライズシステムにおいては、そもそも設計レベルでupdateやdeleteを前提としない設計をする一派もいる
- 基本的な考え方は、rdbに対してinsertとselectしかしないという考え方
- insertしかしないのであれば、そういう縛りを与えることでなにが起こるかというと、改竄は発生しない、情報が失われることはない
- データをどうやって情報として切り取るかというselect文をちゃんと書くことができれば事実は絶対にdbにあるということを断言できる
- 情報を積み重ねて、最新のデータを引っ張れば最新の状態がわかる
- そうした考え方は1980ー1990年代はrdbを中心としたエンタープライズシステムの設計と方法論が盛んだったころ
- できごとを積み重ねていくデータ量が心配
- 簡単に言うと、データが増えるなら金を積むというソリューションがひとつ
- もうひとつは、あとは時代的なものでいうと、レコード量で言うと、webシステムがでてきて以降のデータ量と、企業のデータ量は、エンドユーザーの数は社員数で圧倒的に増えるじきがあるものでないし無限にスケールしなければいけないものでもない
- webに面してるカスタマー向けのシステムはユーザもレコード量も増え続ける
- そうしたシステムの設計と、企業内のシステム 企業内のシステムはあえていうならば速いシステムより堅牢なシステムやデータが失われないシステム設計のほうが大事
- 設計の力点の置き場所が変わってくる
- そうした設計手法もあった
原理主義的に適用してしまうと、現役のデータは常に最新のデータを持ってこないといけない
- 常にorder byしなければいけない
- webでやるとつらい
- 一番最新のデータが入ってくるデータを設けたいということになり、だんだんおかしなことになったりする
最近で言えば基本的にはupdateなしに業務アプリケーションをつくっていくという考え方は、たとえば川島さんという方がimmutable data modelという名前で説明している
- 更新日時というカラムがあったらなにかおかしいと思え、という警鐘をならす
- 現代のシステムはパフォーマンス要件を満たさなければいけないので、どこは更新を許さず、どこは更新を許すか、色付けして判断をできていくようにしましょうという考え方
- 論理的で妥当
- そもそも事実は消されることはない
3つのソリューションで解決できない問題
- 3つのソリューションで1つだけ解決できていない問題がある
- 誤った操作をなかったことにしたい、誤った操作をすぐ元に戻したい
- ステータスであれば削除した状態を削除済み状態を元に戻すができる
- 短い時間で元に戻したい
- 誤操作はある
- 教科書的なことをいうなら、まちがえにくいuiをつくりましょう
- そもそも誤操作を情報設計、画面設計をしましょう
- 大事な情報のサブミットする前にダイアログやアラートや確認画面を出しましょう
- 確認画面は日本のシステム設計において、すごくよくある要件
- 海外のシステムには確認画面がないことが多い
- 確認画面は、一定割合の人は確認画面で気付いて戻ってくれるが、ほんとに誤操作する人は確認画面も確認せずにとばす
- ほんとに確認画面やダイアログでほんとに誤操作をゼロにはできない
遅延レプリケーション
- きちんとした画面設計やインタラクションの設計以外で誤操作をなくす手段はあるか調べたなかで出てきたのは、遅延レプリケーション
- 普通はレプリケーションはほぼリアルタイムでプライマリからセカンダリに更新を伝播していく
- 更新がはやいほど望ましい
- 遅延レプリケーションは、わざとひとつのセカンダリデータベースだけ時間差をつけてレプリケーションをするというソリューション
- ひとつだけ10分遅れてレプリケーションをするdbを用意しておく
- 常に10分前の状態
- 10分間だけ、物理削除されてデータも、物理削除前の状態も残っている
- 誤操作によって物理削除されたものをどうする、というソリューションが必要になった時に、物理削除されたものを戻してくるのは、dbのバックアップファイルから戻すのかとか、とたんに難易度があがってしまう
- バックアップやログから戻すのでなく、レプリケーションをひとつだけ遅らせてデータを持ってくればよいというソリューションはなくもない
- だいぶ重いやり方
- 事故の対応のときだけ、アクシデントからの復帰からしか使わないセカンダリの遅延レプリケーションのdbを使う
- 実案件では使ったことがない