⛰️

業務から見たテンポラルデータモデルの解釈と利用方法の紹介

11 min read

FOLIO Advent Calendar 2020の25日目の記事です。

これはなに

金融機関は業として金融商品や為替の取引を行ないますので、それに付随してお客様のお金や証券といった保有資産を管理が必要となります。
お客様の資産ですので1円でもズレることがないよう、厳密な管理が求められます。
特に顧客資産を含むようなデータの履歴管理は、(意識的, 無意識を問わず)不正な操作が行なわれていないことを担保するために重要です。

一方でデータの履歴管理を実現するデータモデルとして、テンポラルデータモデルが存在します。
テンポラルデータモデルは履歴管理が可能ではあるのですが、寡聞にしてどのような業務にどのテンポラルデータモデルを適用するべきかについて述べられた日本語記事はないように思われます。
実際に私が関わったシステムでテンポラルデータモデルを採用したのですが、どの業務でどのテンポラルデータモデルを適用すれば良いのか、そしてその理由はなぜかというところが曖昧だったため、実装を進めた時にチームが混乱し手戻りが多く発生しました。

そこで本稿では、テンポラルデータモデルを適用する条件と元となる業務の解釈について、実務上で得られた経験を元に紹介していきたいと思います。
私なりの解釈で簡易な例を使って説明を致しますので、間違いや不正確な表現が多々あるかと思います。
どうか広い心でご笑納頂ければ幸いです。

業務をテンポラルデータモデルで表現する

ここでは、簡単にテンポラルデータモデルの私なりの解釈と実務上の適用範囲を紹介します。

実時間と仮想時間

実生活で意識されることはないと思いますが、一般的な業務フローは2つの時間を使って構築されています。
実時間と、ビジネスの都合上導入される概念上の時間です。
後者の良い呼び方を知らないので、本稿では「仮想時間」と呼びます。(良い呼び名をご存知の方、ぜひ教えてください。)
「実時間」は一般的に時間として想像されるもので、止まることなく流れ、巻き戻しや早送りは出来ません。
一方、「仮想時間」はあくまで概念上の時間なので、自由に操作が可能です。

例えば今日2020/12/20の日曜にあなたがショップXの商品の購入のため銀行振込を行なったとします。
すると振込の画面のどこかに「振込は12/21に行なわれます」という風な注意書きが書かれるかと思います。
この「12/21」というのが仮想時間の一例です。
どういうことかというと、「振込」という処理は大まかにいうと、

  • 銀行Aのあなたの口座からの出金
  • 銀行Aから銀行Bへの現金の移動
  • 銀行BのXの口座への入金

という一連の処理に分割され、全て完了して初めて「振込」が完了といえるわけですが、実務上は「処理が全て完了すること」 = 「振込の完了」とはなっていません。
もし処理が全て完了を「実時間」で待つこととすると、膨大な数のしかも様々な国内外の口座への移動を全て追跡することになり、業務が破綻するからです。
そこで、それぞれの処理の「実時間」における完了時刻は別途存在するにも関わらず、「仮想時間」における「12/21」という仮想的な完了時刻に、「実時間」ではまだかもしれないのに振込が完了したものと見做しているのです。(見做しているだけので、時々どこかの処理が失敗することがありえます。)

他にも例えば、一週間前に行なった入金トランザクションが実は不正だった、1000円入金のところを100円しか入金していなかった、ことが判明したとしましょう。(怖いシチュエーションですね)
不正な入金を取り消して正しい入金を行なう必要があるのですが、やり直しなので入金は一週間前に行なったことにする必要があります。
もちろん「実時間」は巻き戻せませんので、「仮想時間」を一週間前に戻して、過去の状態からあらためて正しいトランザクションを適用するというようなオペレーションが必要になります。

このようにビジネスの世界では、「実時間」とは別の「仮想時間」の概念をコンテキストにあわせて導入し、業務フローが構築されています。

テンポラルデータモデルと実時間・仮想時間

テンポラルデータモデルは、この「実時間」と「仮想時間」を表現するデータモデルです。
BiTemporal Data Modelに入門中での表現との対応は以下のようになります。

  • 「仮想時間」 = “valid-time”
  • 「実時間」 = “transact-time”

ここからは「仮想時間」と「実時間」のそれぞれを valid-time, transact-timeと呼ぶことにします。
テンポラルデータモデルはvalid-time, transact-timeの有無の組み合わせで、以下の4つのモデルが定義されます。

モデル valid-time transact-time
Snapshot Model なし なし
Valid Time Data Model あり なし
Transactional Data Model なし あり
Bitemporal Data Model あり あり

テンポラルデータモデルの表現力

先ほども述べた通り業務はvalid-timeとtransact-timeを含んで構築されています。
テンポラルデータモデルはこの2つを取り扱えますので、業務を漏れなく表現できる = 業務の履歴が残ることになる訳です。
冒頭で述べた通り、金融ドメインではデータの履歴管理が求められると言いましたが、適切にテンポラルデータモデルを適用すれば要件を満たせることになります。

では例としてvalid-time, transact-timeの両方を持つBitemporal Data Modelで、以下のような手順の発生する業務を表現してみましょう。

  1. 12/1に初期状態Aが作成された
  2. 12/2に中間状態Bに変更された
  3. 12/3に完了状態Cに変更された
  4. 12/4にBの誤りが発覚し、中間状態B', 完了状態C'に修正した

まず手順1では状態Aが作成されます。

横軸はtransact-time(実時間)、縦軸はvalid-time(仮想時間)です。
状態のそばにある座標のようなものは状態の開始時点と有効期限を表しており、それぞれvalid-time基準での有効化日時(上段左)とその期限(上段右)、transact-time基準での有効化日時(下段左)とその期限(下段右)です。
以降はそれぞれを valid-from, valid-to, transact-from, transact-toと呼びます
この時点では状態Aはvalid-from=transact-from=12/1、valid-to=transact-to=∞で、valid-time, transact-timeの両方で最新状態を示しています。
次に手順2で状態Aから状態Bに「変更」されます。

この時のデータの操作は、以下の通りです。

  1. 状態Aのvalid-toを12/2に更新 (i.e. valid-timeから見ると12/2には状態が無効化されている)
  2. 状態Bをvalid-time, transact-timeの両方の最新状態として作成

手順3も手順2と同様に状態Bから状態Cに「変更」されます。

以上で手順3までが表現できました。

実はここまでレコードの作成以外は、全てvalid-toを修正する操作でした。
なぜtransact-toではなくvalid-toなのかが気になるかと思いますが、一旦先に進みます。(その方が説明しやすいので)

次の手順4ですが、実は状態Bが間違えていた事が後から判明したので「修正」を行ないます。(これまでは「変更」でしたが、ここでは操作が異なるため意図的に「修正」と別名で呼んでいます)

「修正」では何が行なわれたかというと、「変更」で作成された A → B → C という履歴をtransact-toを使って論理削除し、 A → B' → C' という履歴を新規登録するという処理になります。

  1. 状態B & Cのtransact-toを12/4に更新 = 古い履歴の論理削除
  2. 状態B' & C'を新規作成 = 新しい履歴の新規作成

A → B' → C のような特定の状態だけ修正することも可能ですが、あまり業務上の用途が思いつかない(過去の状態が変化したのに、それに依存した状態が変わらないことは少ない)のと、 B' → C を新規登録すれば同じ効用は得られるので割愛します。

これで手順4まで完了しました。
上記の絵のとおり、手順1~4までの全てのデータ(A, B, C, B', C')とその操作の履歴(日付)が記録されていることが分かるかと思います。
このように「変更」をvalid-time、「修正」をtransact-timeへの操作として表現することで、業務がBitemporal Data Modelで漏れなく表現できる事が分かりました。
そのほかの3つのモデルは「変更」と「修正」のどちらか一方または両方の操作が不可となります。

「変更」=状態遷移, 「修正」=論理削除

最後になぜ「変更」がvalid-timeで、「修正」がtransact-timeへの操作としているのかを、私なりのイメージを紹介して本項を終わりたいと思います。
「修正」という名前からは、業務としては「誤った状態を消して、正しい状態が入力されること」=論理削除 + 新規登録を期待されるのが一般的かなと思います。
一方「変更」という名前からは、業務としては「古い状態の後に新しい状態を登録すること」= 状態遷移が期待されるかと思います。
そのため、「今日はどんな状態だっけ?」「去年の12/20はどんな状態だったっけ?」というように、最新の状態や過去の状態など遷移を参照することがありえます。

先ほど述べたとおりtransact-timeは「実時間」に相当しますので、「実時間」は巻き戻ることがありませんので、transact-toに現在時刻を設定するというデータ操作は論理削除と等しくなります。
一方でvalid-timeは「仮想時間」に相当しますので、巻き戻しや早送りなどの操作が可能で状態遷移を表現できます。
よって「変更」=状態遷移の操作はvalid-timeの操作、「修正」=論理削除はtransact-timeの操作として解釈することがより自然となり、インピーダンスミスマッチを回避しやすくなるかなと思います。

どちらの時間軸においても削除は、「変更」「修正」で登録をする新しい状態がnullであるのと同じなので、特に区別して議論はしていません。

テンポラルデータモデルの利用例の紹介

ここまで長々と業務とテンポラルデータモデルの紹介をしてきました。
最後によりイメージを掴みやすくするため、具体的に私がどのようにテンポラルデータモデルを適用しているか、例を挙げていきたいと思います。

利用するモデル

先ほどの項でテンポラルデータモデルには4つのモデルがあるという話をしました。
が、実務で用いているのは Snapshot Model, Transactional Data Model, Bitemporal Data Modelの3つがほとんどです。
その理由ですが、冒頭で述べた通り設計の対象としている業務はデータ操作の履歴を残す事が重要です。
そのためtransact-timeの時間軸がなく論理削除が出来ないValid Time Data Modelは、あまり使い道がありません。
論理削除が不要で絶対に正しいデータ、例えばある株式の取引所の価格の推移のような、正解を誰かが保証してくれる状態遷移のデータに用いることになるかと思われます。

書いていて気づきましたが、後述するマスタデータの例のレプリカ側はValid Time Data Modelで保持していると、レプリカであることが明示されていて良いかもしれません。

テンポラルデータモデルでのデータ保管

では実際に株式の購入注文を例にして、テンポラルデータモデルでのデータ保管を見ていきましょう。

Snapshot Model

何度か述べている通り、金融ドメインのデータの履歴管理が重要なため、Valid Time Data Modelと同様に使われる頻度が低いモデルです。
購入注文のメッセージ(コマンド)をキューにpushするというのは、Snapshot Modelの一種と言えます。(メッセージそのものは論理削除も状態遷移も発生しないので)

レイテンシの関係でメッセージキューを使ったEvent Sourcingなシステムを構築せざるをえない事もあります。
その場合はSnapshot Modelを前提とした制約が発生します。
これは「変更」や「修正」といったデータ操作をしたい場合には、「注文」というメッセージだけでなくそれぞれの操作に相当するメッセージが必要ということです。
この実例として、金融業界のデファクトスタンダードであるFIXプロトコルが挙げられます。
Snapshot Modelは他にも実務上、顧客資産に関係がないもので履歴管理が必要がない重要度の低いデータにこのモデルを適用しています。

Transactional Time Data Model

transact-timeだけを持ったこのモデルは、論理削除が使えるため一番よく適用されています。
例として購入注文AとBの2つで考えてみましょう。

先ほど登場したキューに購入注文AとBのメッセージ(コマンド)が投入されました。
ところが何らかの理由、 例えば購入資金が不足していたのにバリデーションで弾けなかった etc、で注文Bを誤って受け付けてしまったことが判明します。
注文Aは購入処理を進めたいが、注文Bは不正なデータなので「修正」(論理削除)したいという状況ですね。
このような業務に対応するために、Snapshot Modelであるキューの注文データを、Transactional Time Data Modelの注文データに変換して保存しておくことが一つの方法です。
データモデルを変換してtransact-timeを持たせておくと、万が一不正なメッセージやコマンドが連携された時でもtransact-timeで論理削除することで、後続の処理が不正なデータを無視して処理を進めることが可能になります。

Bitemporal Data Model

transact-timeとvalid-timeを持ったこのモデルは、先ほども述べた通り状態遷移が表現可能なモデルです。

これまでと同様に購入注文を例として考えてみましょう。
先ほどの例で購入注文Aの注文データはTransactional Data Modelで保存しました。
が、一般的に注文は数量の訂正や取消といった「変更」の操作が行えるケースが多いです。

エンドユーザーの指示による注文の訂正や取消は、注文がなかったことになることを期待している訳でなく、注文が取り消されたことが確認可能なことを期待するかと思います。
これは状態遷移が履歴として残ることが期待されているということなので、「修正」ではなく「変更」の操作に分類するのが自然です。

訂正のリクエストは最初の例にあったキュー経由で送られてくるとして、注文Aというエンティティに紐づく注文データを、訂正を受けて初期状態である#1から#2へ遷移させる必要があります。

そこで先ほど注文データは Transactional Time Data Modelを採用しましたが、valid-timeも追加してBitemporal Data Modelに拡張すると自然に履歴管理が可能となります。

購入注文を例として、Snapshot Model, Transactional Time Data Model, Bitemporal Data Modelでのデータ保管を見てきました。
なんとなくイメージを掴んでいただけたなら幸いです。

テンポラルデータモデルのデータ参照

データ保管を見てきましたが、保管したデータは誰かに参照され使われることが前提です。
実際に導入した際もデータの保管はスムースだったのですが、いざデータを参照して他の処理を進めようとなった時に結構混乱しました。
最後に私が設計するシステムでのデータ参照の用途ごとの検索条件を紹介して終わりたいと思います。

用途 valid-time条件 transact-time条件
最新の状態を取得 valid-to = ∞ transact-to = ∞
最新の状態遷移を取得 無指定 transact-to = ∞
過去の状態を取得 valid-from <= 「参照時刻」 < valid-to transact-to = ∞
ある時点の断面を取得 無指定 transact-from <= 「参照時刻」< transact-to

最新の状態を参照 & 最新の状態遷移を取得

テンポラルデータモデルのデータから最新の状態を検索する際、素直に考えると valid-from <= now < valid-toのような条件で検索したくなると思います。
nowはサーバー時刻を使うかと思われますが、インフラ構成によってはデータ保管する処理とデータ参照する処理が異なる物理サーバーで稼働することがあります。
そうするとデータ保管を行なうサーバーXの時刻が、参照を行なうサーバーYの時刻よりも進んでいる場合、正しい断面で最新状態が参照できないようなことが起こり得ます。
以下の例だと、サーバーYが参照する時刻Tにおいて既に状態#2に遷移しているので、参照結果も状態#2が期待されますが、サーバーYの時刻TはサーバーXではT+αのため、時刻Tで参照すると状態#1が参照されることになり、不具合の原因となりかねません。

全ての処理を同一サーバー時刻で実行すればこういった問題は起きないかもしれませんが、アプリケーションの設計において特定のインフラ構成を前提とするのは好ましくありません。
そののため、最新の状態または状態遷移を参照したいというコンテキストでは、原則サーバー時刻は使わずに∞条件での検索とした方がベターかと思います。

過去の状態を取得

過去の状態を参照する場合は、素直に valid-fromm <= now < valid-toを検索条件とします。
ただし”参照時刻時点で有効な注文を全て取得”というような用途の場合、「参照時刻」として valid-fromよりも前の時間を指定したことにより取得出来ず、処理対象から漏れてしまうというような運用上のミスが発生しがちでした。
「参照時刻」を正しく設定することはもちろん大事ですが自明ではなく間違いやすいので、間違うことを前提として、リコンサイルなど漏れを検知するような機構を別途用意して、ミスにすぐに気づけるようにしていました。

ある時点の断面を取得

valid-timeは状態の検索に用いられる一方、transact-timeは断面の取得に用いられます。
例えば祝日情報のようなマスタデータを配布する場合、単に最新の状態を参照してしまうと、データの完全性が担保されない可能性があります。
そこで、ある時刻の断面をコピーする必要がありますが、valid-timeでフィルタリングしてしまうと状態遷移の情報が欠落してしまいます。

そのため、テンポラルデータモデルの断面を取得する場合は、もう一つの時間であるtransact-timeの軸でフィルタリングをすることになります。
システムAのデータをtransact-timeを参照時刻でフィルタした結果作成された断面は、Valid Time Data ModelまたはSnapshot Modelのデータとなります。
この断面をシステムBに取り込む時にtransact-timeを追加することで、システムAからシステムBへのデータコピーが完了となります。

この時、transact-timeとして設定する時刻はシステムAのtransact-timeではなく、システムBで状態を登録する際の時刻 = コピー時刻とするのが自然です。(transact-timeはシステム= コンテキストごとに独立して存在し、valid-timeはビジネス上の概念のため、業務を構成するシステムを跨いで共通)

記事を書き進めているうちに気づきましたが、システムAがマスタデータのオリジナル=正解を持つなら、システムBではValid Time Data Modelを採用するというのも自然かと思います。
弊社でtransact-timeを付加する理由を書いておくと、テスト環境や本番環境テストで都合の良いようにデータの「修正」を行ないたいことからtransact-timeを付加するようにしています。

最後に

最初にテンポラルデータモデルの導入にあたってはチームメンバーとのミスコミュニケーションが発生、議論を経てようやく整理されていきました。
本稿ではミスコミュニケーションの原因となっていた時間軸の複雑さを少しでも解きほぐすため、経験から得られた自分なりの解釈を紹介しました。
また設計の際に使っているモデル選択の基準や、実は結構難しくバグの原因になりやすいデータ参照方法についても紹介しました。

テンポラルデータモデルは実際に使ってみるととても有効です。
特に物理削除や直接更新が不要となったことで、データの復元や事後の操作履歴の検証が容易になり、運用の難易度は格段に下がったと感じています。
一方で設計の難易度や開発コストが高めですし、脳死で設計してしまうと単に運用コストが上がるだけだったりします。
皆様がテンポラルデータモデルを検討される際に、本稿が少しでも助けとなりましたら幸いです。

Discussion

ログインするとコメントできます