僕はデータを複製して持ちたくないとずっと思っているけど、 色んな意見聞いてると自分でもそれが本当に良い事なのかわからなくなりそうだったので、 なぜ複製が嫌いなのか書きながら考えを整理してみた。
- データを複製すると不整合が起きるから嫌だ
- 計算結果を持つのもデータ複製の一種と考えている
- あるデータ、ある処理に変更を加えたときに「関連してこっちも変更しないといけない」というのをできるだけ意識したくない
- 算出による処理速度が問題になる場合はキャッシュを使う
- 複製して持つべきものもある
- スナップショットも導出できるならするべき?
- 外部サービス連携はデータ複製してもっているのでは?
データを複製すると不整合が起きるから嫌だ
ECショップシステムにて
「商品マスターデータ一覧から商品を選んで[販売する]ボタンを押すと、ECショップに商品が表示されて販売開始される」
のような機能があった時、以下のようなテーブル設計を考えてみる。
データ的には 商品マスターから販売商品テーブルにデータコピーされてる。
販売終了時は販売商品テーブルから削除する
商品マスター
id | 商品名 | 価格 |
---|---|---|
1 | A商品 | 1000 |
2 | B商品 | 2000 |
販売商品
id | 商品名 | 価格 | 商品マスターid |
---|---|---|---|
1 | A商品 | 1000 | 1 |
A商品の値段が上がったので商品マスターの値段を変更した。
でもECショップに表示されている商品金額は変わらない。
商品マスター
id | 商品名 | 価格 |
---|---|---|
1 | A商品 | 5000 |
2 | B商品 | 2000 |
販売商品
id | 商品名 | 価格 | 商品マスターid |
---|---|---|---|
1 | A商品 | 1000 | 1 |
このタイミングでお客さんが1000円と表示されているA商品を購入した。
しかしなぜかお客さんには5000円で請求されてしまった。
処理コードを見てみると、請求では商品マスターテーブルの金額を参照していた。。
その後商品名が変わった(A商品 => A商品Ver2)のでECサイトで表示を変更するためデータを変更した。
この時、商品マスター側を変更しなければならないのに気が付かなかった。
商品マスター
id | 商品名 | 価格 |
---|---|---|
1 | A商品 | 5000 |
2 | B商品 | 2000 |
販売商品
id | 商品名 | 価格 | 商品マスターid |
---|---|---|---|
1 | A商品Ver2 | 1000 | 1 |
しばらくして一旦商品の販売を停止した。
id | 商品名 | 価格 |
---|---|---|
1 | A商品 | 5000 |
2 | B商品 | 2000 |
販売商品
id | 商品名 | 価格 | 商品マスターid |
---|---|---|---|
その後再度商品販売を開始したら、商品名が古いものになってしまった
商品マスター
id | 商品名 | 価格 |
---|---|---|
1 | A商品 | 5000 |
2 | B商品 | 2000 |
販売商品
id | 商品名 | 価格 | 商品マスターid |
---|---|---|---|
1 | A商品 | 5000 | 1 |
今回の例だとシンプルなのですぐにデータの関連が理解できるけど、データコピーを繰り返しているとDB上に同じような意味に見えるデータが散在するので、どこでどのデータが使われているのかよくわからなくなっていく。
僕はデータを複製して持ちたくないので、以下のようにしたい。
商品マスター
id | 商品名 | 価格 |
---|---|---|
1 | A商品 | 1000 |
2 | B商品 | 2000 |
販売商品
id | 商品マスターid |
---|---|
1 | 1 |
A商品の値段が変わったので、商品マスターの金額を変更した
商品マスター
id | 商品名 | 価格 |
---|---|---|
1 | A商品 | 5000 |
2 | B商品 | 2000 |
販売商品
id | 商品マスターid |
---|---|
1 | 1 |
ショップ表示金額は商品マスターから取得されてるので5000円と表示される。
お客さんは5000円で商品を購入し、請求は5000円で行われる。
計算結果を持つのもデータ複製の一種と考えている
合計金額とか算出できる結果を持つのもデータ複製だと考えている。
計算元に変更があると、再計算しないといけない。
以下の例の請求テーブルの請求金額は、請求詳細テーブルの金額の合計となっている。
請求
id | 顧客id | 請求金額 |
---|---|---|
1 | 1 | 300 |
請求詳細
id | 請求id | 商品id | 金額 |
---|---|---|---|
1 | 1 | 1 | 100 |
2 | 1 | 2 | 200 |
なんかの事情で[商品id: 2]の商品を請求しないことになったので、データを修正した。
でも請求金額の調整(もしくは請求金額計算処理の再実行)を忘れていたので結局300円で請求されてしまった。。
請求テーブル
id | 顧客id | 請求金額 |
---|---|---|
1 | 1 | 300 |
請求詳細テーブル
id | 請求id | 商品id | 金額 |
---|---|---|---|
1 | 1 | 1 | 100 |
僕はデータ複製したくないので以下のようにしたいと思ってる。
請求合計金額はロジックが算出する。
請求テーブル
id | 顧客id |
---|---|
1 | 1 |
請求詳細テーブル
id | 請求id | 商品id | 金額 |
---|---|---|---|
1 | 1 | 1 | 100 |
2 | 1 | 2 | 200 |
なんかの事情で[商品id: 2]の商品を請求しないことになったので、データを修正した。
合計金額は正しく100円で算出された。
請求テーブル
id | 顧客id |
---|---|
1 | 1 |
請求詳細テーブル
id | 請求id | 商品id | 金額 |
---|---|---|---|
1 | 1 | 1 | 100 |
あるデータ、ある処理に変更を加えたときに「関連してこっちも変更しないといけない」というのをできるだけ意識したくない
上記の例でデータ変更を行った時に問題が発生したのは「関連してこっちも変更しないといけない」の対応が漏れていたから。
でもどこが関連しているのかは、ロジックを知っていないとわからない。
これを洗い出す作業は結構大変だと思ってる。
データ複製をしていなければ、変更の影響は即座に参照している部分に反映される。
算出による処理速度が問題になる場合はキャッシュを使う
こんな感じでデータ複製を持たず毎回算出させていると処理速度が問題になってくるケースはあると思う。
その場合は「計算結果をキャッシュとして持たせる」のはアリだと考えている。
ただしキャッシュは「データ複製」に当たるので不整合が発生するのでそのへん気にしないといけない。(このあたりがキャッシュが難しいといわれてる部分だと思う)
「結局データ複製をするならキャッシュでやろうが、テーブル内でやろうが同じなのでは?」と一瞬思ったけど違う。
キャッシュは消えても問題ないデータとして設計される。
だから不整合解消の方法が明確。
基本的にキャッシュ再生成を実行すれば整合性の取れたデータでキャッシュが生成されて不整合が解消される。
テーブル内にデータ複製を持つ設計はキャッシュとしては設計されていない場合が多い。
だから不整合解消の方法はケースバイケースで、単純に再実行すれば正しいデータが上書かれて整合性が取れる場合もあれば(これはキャッシュとして機能していると言えるのかもしれない)、新しいレコードが出来てしまってだめな場合もある。
また、その複製データがキャッシュなのかそうでないのかがわかりやすい形で実装されている必要がある。
キャッシュデータはRDBではなく、redisのようなインメモリDBとかに入れる、みたいに保存場所を分けておけばそれが明確になる。
複製して持つべきものもある
例えば、お客さんの購入履歴テーブルがある。
ここには商品名と商品金額が商品マスターから購入時点で複製されて入っている。
ある日、A商品を買った場合
商品マスター
id | 商品名 | 価格 |
---|---|---|
1 | A商品 | 1000 |
2 | B商品 | 2000 |
購入履歴
id | 商品id | 商品名 | 商品金額 | 購入日 |
---|---|---|---|---|
1 | 1 | A商品 | 1000 | 2020-1-1 |
その後、ECショップでA商品の金額が変わった。
購入履歴の金額は1000円のまま。
これは正しい挙動と言える。
商品マスター
id | 商品名 | 価格 |
---|---|---|
1 | A商品 | 5000 |
2 | B商品 | 2000 |
購入履歴
id | 商品id | 商品名 | 商品金額 | 購入日 |
---|---|---|---|---|
1 | 1 | A商品 | 1000 | 2020-1-1 |
複製してなかったら、商品マスターを見に行って
「1000円で購入したはずなのに5000円で購入したことになってた」
となってしまう。
この違いはなにか?
うまく説明できなかったのだけど「その時起こった事実を残さねばならない」という類のデータは、スナップショットとしてデータコピーして持つべき、という感じなんかなと思った。
ログに近い性質を持ったデータ。
スナップショットも導出できるならするべき?
「購入履歴の金額や商品名も、商品マスターにデータ変更履歴を持たせていれば導出できる。 そうであれば、これらもデータを保持するんじゃなくて導出すべきなのでは?」
と言われてたしかになと思った。
ただしこれはログ的な意味が強いと思っていて「あの時点で実際にどういう値で処理が行われたのか」という事実を記録しているもので、
ここを導出で導くのとデータコピーをして持っておくのと、どちらが信頼性が高いかと言えばデータコピーであると思うので、データ複製で持っておくべきかなと考えている。
導出されるようにしておけば、処理の再実行も可能になるので、この問題とは別問題としてあったほうがより良い気はする。
外部サービス連携はデータ複製してもっているのでは?
「外部サービス連携するときって、向こうはデータコピーを持ってる事になるよね?システム内でデータコピーしてても、それって内部でやってるか、外部でやってるかの違いでしかなくない?」 と言われてたしかに、と思った。
例えば商品マスタデータは社内に持っていて、外部のECサービスで商品販売をしてるとすると、商品マスタデータに変更があるたびにECサービスとデータ連携を実行しなければならない。 ECサービスにデータがコピーされて保持されていて自動的に連携されないから。
じゃあ内部で完結する場合に、上記のようにデータ連携処理実行をするような構成にしたほうがいいのかと考えるとそうではない気がする。 余計な処理をしなくても勝手に整合性がとれた状態になってほしい。 外部サービスはそう出来ないから「データ連携処理の実行」をやるしかない、という認識。
外部サービス側のデータコピーは「キャッシュ」に近い特性がある気がする。 境界線と整合性を取るための手段が比較的ハッキリしている。
内部でもってる複製データは、後々それだけみても複製されたデータなのかどうかさえロジックを追わないとよくわからない。