May 2016

Volume 31 Number 5

Cutting Edge - 履歴 CRUD の構築

Dino Esposito | May 2016

Dino Espositoリレーショナル データベースは 1970 年代に登場し、その時代にキャリアを開始した数世代の開発者は、そのキャリアを終えるまでに、データ ストレージに対する別のアプローチを学ぶことも、考えることもありませんでした。最近、急成長してきたソーシャル ネットワークによって、リレーショナル データベースでは対処できないビジネス シナリオがあることが実証されました。スキーマを持たないデータが (本当に) 膨大な量送られてくると、リレーショナル データベースは、パイプの役割を果たすどころか、ボトルネックになることがあります。

友人が投稿にコメントした「いいね!」の数を、総レコード数が数十億にも及ぶ包括的なリレーショナル データベースで瞬時にカウントすることを想像してみてください。少なくとも、投稿の定義を厳格なスキーマに落とし込むのは並大抵のことではありません。単にビジネスの生き残りを懸けた問題と捉えれば、いつの間にかソーシャル ネットワークが進化し、ストレージはリレーショナル データ ストアと非リレーショナル データ ストアが混在する方向に進み、結果として、多種多様な構造のデータを扱うビジネスが正式に始まっています。

ソーシャル ネットワークのソフトウェア アーキテクチャから得られる根本的な教訓は、データのプレーンなストレージはビジネスに適した理想的なアプローチとは言えない場合があるということです。手にしたデータをすべて保存するのではなく、発生したイベントとそのイベントに関連するデータについての詳細を保存する方が適切だと言えます。

今回は、まず、アプリケーションの主要データ ソースとしてログに記録したイベントを使用する、イベント ソーシングのビジネス基盤について詳しく調べます。その後、これまでの CRUD (作成 (Create)、読み取り (Read)、更新 (Update)、削除 (Delete)) のスキルを、イベントを踏まえた CRUD に刷新する方法を紹介します。最初から論点を明確にしておくと、イベント ソーシングが必要かどうかが問題なのではありません。イベント ソーシングがいつ必要になり、どのようにコーディングするかが問題です。

動的データ モデルに向けて

最近注目されている話題は、多種多様な構造のデータを扱うことです。つまり、構造化されたデータにはリレーショナル データベース、構造化されていないデータには NoSQL データ ストア、ユーザー設定やログにはキーと値のディクショナリ、関係や相関関係にはグラフ データベースを利用する考え方です。さまざまなストレージ モデルを導入し、サイドバイサイドで運用するのは方向性としては正しいといえますが、根本的な対策というよりは、対処療法的な対策に思えます。

リレーショナル モデルが 10 年以上にわたって効果的だと考えられてきたのは、データの読み取りと書き込みのメリットのバランスが取れていたためです。リレーショナル モデルでは照会と更新が簡単です。(非常に) 極端な条件で多少の制限があるとしてもです。テーブルに数百万件のレコードや数百の列が含まれると、全体的なパフォーマンスが目に見えて低下します。さらに、データのスキーマは固定で、アドホックで高速のクエリを作成するにはデータベース構造の知識が必要になります。つまり、今日のコーディング環境では、事前準備を必要とする巨大なリレーショナル モデルのような包括的なモデルは、まず表現力に限界が生じ、やがてはプログラミング能力にも影響する大きな制約になります。結局、モデルはモデルにすぎず、現実を直接表すものではありません。現実の世界には、どのようなモデルも見受けられません。しかし、モデルを使用すれば、十分に理解している反復可能な動作をカプセル化できます。結局のところ、現実には発生しているイベントを目にしているのに、そのイベントに関連する情報だけを制約のある (リレーショナルな) モデルに押し込もうとしているのです。これが事態を困難にしていることが判明したため、スキーマやインデックスの制約を受けない別のストレージ モデルを探しているのが現状です。

イベントベースのストレージ モデル

この数十年間、エンティティの現在状態だけを保存するのは便利で強力な方法でした。特定の状態を保存すると、既存の状態は上書きされ、以前の情報はすべて失われます。この動作には問題もありますが、メリットもあります。当時は、効率的なアプローチとして幅広い支持を得ていました。過去の状態がすべて失われることを受け入れられるかどうかを問題にできるのは、関係するビジネス領域と顧客だけです。長い間、大半のビジネスで受け入れられていたことが、この事実を物語っています。しかし、こうした傾向が変化しています。多くのビジネス アプリケーションで、ビジネス エンティティの履歴をすべて追跡することが求められるようになっています。長年 CRUD (プレーンな作成、読み取り、更新、削除の操作) と呼ばれきたもの、つまり、プレーンなリレーショナル テーブルを土台にその上位にモデル化されてきたものが進化を遂げ、履歴 CRUD と総称されるようになっています。履歴 CRUD とは単なる CRUD コードベースのことで、その実装では、変更の全リストの追跡が管理されます。

現実の環境には、ドメイン内で実行されたイベントを何らかの方法で追跡する基幹業務 (LoB) システムがたくさんあります。この種のアプリケーションはこの数十年間にも存在し、COBOL や Visual Basic 6 で書かれたものもありました。たとえば、財務アプリケーションでは、日付や住所の変更や、貸方伝票の発行など、請求時に発生する可能性のあるすべての変化を追跡します。こうした一部のビジネス シナリオは監査対象の機能に分類され、ソフトウェアの初期の頃からイベントの追跡が必須機能とされてきました。

したがって、ビジネス イベントの監査は、ソフトウェアにとっては新しい概念ではありません。この数十年間、開発チームは同じ問題を何度も解決し、見つかる限りの最高の方法で既知の技術を改め、作り直してきました。昔からある優れたビジネス イベント監査のプラクティスは、現在、「イベント ソーシング」という魅力的な名前の下に受け継がれています。

ビジネス イベントのコーディング

概念を示すために、ユーザーが共有リソース (会議室) を予約するシンプルなアプリケーションを考えます。ユーザーがある予約状態を確認するときに、その予約の現在状態だけでなく、予約を行ってからその予約がどのように更新されたかを示す完全なリストがあると便利です。図 1 は、この考え方を時系列の UI として表しています。

予約の履歴全体の時系列の表示
図 1 予約の履歴全体の時系列の表示

プレーンな状態を基にする CRUD ではなく、履歴 CRUD として動作する予約データ モデルを設計するには、どうすればよいでしょう。テーブル定義に列を追加するだけでは不十分です。CRUD と履歴 CRUD の主な違いは、履歴 CRUD が特定の時刻に発生したビジネス イベントごとに 1 つずつ、同じエンティティのコピーを複数格納する点です。図 2 は、リレーショナル データベースの予約テーブルとして採用できる新しい構造を示します。

履歴 CRUD アプリケーションに使える可能性があるリレーショナル データ モデル
図 2 履歴 CRUD アプリケーションに使える可能性があるリレーショナル データ モデル

図 2 のテーブルには、ビジネス エンティティの状態を完全に表すために必要になる列に加え、他にもいくつか列があります。最低でも、テーブル内の列を一意に識別する主キー列は用意します。次に、タイムスタンプ列を追加します。この列には、データベースを操作した時刻、またはビジネス上意味のある任意のタイムスタンプのいずれかを入力します。この列は、予約可能な日付をエンティティの状態に関連付ける目的に使用します。最後に、記録されたイベントを説明する列を追加します。

これでは依然、リレーショナル テーブルを使用してアプリケーションが必要とする予約のリストを管理している状態です。新しいテクノロジを追加したわけではありませんが、図 2 に示されているスキーマを持つテーブルは、概念的に、従来の CRUD とはかけ離れています。この新しいテーブルにレコードを追加するのは簡単です。追跡対象のシステム内で何かが起きているという通知を受け取ったときに、レコードに必要な値を設定して、テーブルに追加するだけです。CRUD の「C」については問題ありません。では、他はどうでしょう。

履歴 CRUD での更新と削除

従来のリレーショナル テーブルをイベントベースの履歴テーブルに置き換えると、更新と削除の役割、および両者の関係性が大きく変わります。まず、更新という操作がなくなります。つまり、エンティティの論理状態に対するすべての更新は、新しい状態を追跡する新しいレコードの追加として実装することになります。

削除は扱いにくい操作で、最後はドメインのビジネス ロジックによってコーディング方法が決まります。理想を言えば、イベントベースの環境には削除は存在しません。データは追加されていくだけです。したがって、削除は、エンティティがもはや論理的に存在しないことを示す新しいイベントを追加することにすぎません。ただし、テーブルからデータを物理的に削除することが法律で禁止されているわけではないので、今でも削除は可能です。ただし、イベントベースのシナリオでは、削除対象のエンティティは、1 つのレコードではなく、レコードの集まりになります (図 2 参照)。エンティティを削除することにした場合は、関連するすべてのイベント (およびレコード) を削除しなければなりません。

エンティティの状態の読み取り

アプリケーションでビジネス イベントをログに記録することによって得られる最大のメリットは、不足するものが何もない点です。特定の時点におけるシステムの状態を追跡し、その状態に至るまでの動作の正確なシーケンスを特定して、そのイベント シーケンス全体または一部を取り消すことができます。これは、自作のビジネス インテリジェンス、およびビジネス分析での独自の what-if シナリオの土台になります。もっと正確に言えば、アプリケーションに手を加えずにこうした機能を自動的に取り込むことはできないとしても、既存のアプリケーションを土台にこの拡張機能を開発できるだけのデータは揃っています。

履歴 CRUD で最も難しい部分がデータの読み取りです。今回のサンプル予約システムでは関連ビジネス イベントをすべて追跡するようになりました。しかし、予約が変化する過程を示す完全なリストを簡単に取得できる場所はありません。これを把握する簡単な方法はありません。たとえば、次週の予約数がどの程度になるかはわかりません。これにはプロジェクションが適しています。図 3 は、プレーンな CRUD から履歴 CRUD に進化するシステム全体のアーキテクチャをまとめたものです。

履歴 CRUD システムのアーキテクチャ
図 3 履歴 CRUD システムのアーキテクチャ

イベントベースのシステムは、必然的に、コマンド スタックとクエリ スタックを明確に分けて実装する方向に向かいます。ユーザーは、プレゼンテーション層からタスクをトリガーします。そのタスクは、アプリケーション層、ドメイン層を経由して進んでいきます。こうして進んでいく過程に、ビジネス ロジックのすべての構成要素が関わってきます。コマンドは、システムの現在状態を変更するビジネス タスクのトリガーになります。つまり、何かをコミットして、既存の状態を論理的に変更しなければなりません。イベントベースのシステムでは (それがプレーンでシンプルな CRUD システムだとしても)、状態を変更するということは、ユーザーが特定の予約を作成または更新したことを示すレコードを追加するということです。図 3 で「Event Repository (イベント リポジトリ)」というラベルを付けたブロックは、イベントの保存を担当するコードの層を表します。具体的なテクノロジの点から見ると、この Event Repository ブロックは、Entity Framework ベースのリポジトリ クラス、ドキュメント データベース (Azure DocumentDB、RavenDB、MongoDB) のラッパーにすることができます。さらに興味があれば、EventStore や NEventStore などのイベント ストアの API を使用するラッパー クラスにすることもできます。

イベントベースのアーキテクチャでは、あるエンティティの状態は、要求時にアルゴリズムで導き出します。このプロセスをイベントの再生と呼びます。このプロセスでは、指定したエンティティに関連するすべてのイベントをクエリし、すべてのイベントをエンティティ クラスの新たなインスタンスに適用していきます。この適用のループが終わると、新しいインスタンスは記録されているすべてのイベントを経た状態になるため、エンティティ インスタンスは最新状態になります。

一般的な言葉に変えると、イベントのログを処理することにより、データのプロジェクションを構築し、少量のデータから動的なデータ モデルを抽出します。これが、図 3 で「Read Model (読み出しモデル)」とラベルを付けている部分です。イベントの同じログの上位に、さまざまなフロント エンドとして機能するあらゆるデータ モデルを構築できます。SQL を例えに使うと、ログに記録されたイベントからデータのプロジェクションを構築することは、リレーショナル テーブルからビューを構築することと同じです。

クエリのためにイベントを再生してエンティティの現在状態を判断するのは一般的に有効な方法ですが、イベントの数や要求の頻度が増えるにつれて、効率がどんどん下がっていきます。銀行口座の現在残高を見るために、そこまでの数千件の記録に目を通したくはありません。同様に、保留状態の予約のリストを表示するために、何百件ものイベントを調べたくはありません。こうした問題を回避するため、「Read Model (読み取りモデル)」を従来のリレーショナル テーブルにして、ログ記録されたイベントのテーブルとプログラムで同期するのが一般的です。

まとめ

大半のアプリケーションは、依然として大まかには CRUD アプリに分類されます。ある意味、Facebook は、平均より多少大きな CRUD アプリと言えるでしょう。実際には、ほとんどのユーザーにとっては、既知で良好な最新態があれば十分です。しかし、それでは不十分だと考えるユーザーの数も増えています。次に来るのが最高の顧客かもしれません。今回は、履歴 CRUD についてほんの少し触れただけです。来月は、具体的な例を紹介する予定です。それまで、しばらくお待ちください。


Dino Esposito は『Microsoft .NET: Architecting Applications for the Enterprise』(Microsoft Press、2014年) および『Modern Web Applications』(Microsoft Press、2016年) の著者です。JetBrains の .NET および Android プラットフォームのテクニカル エバンジェリストでもあります。世界各国で開催される業界のイベントで頻繁に講演しており、software2cents.wordpress.com (英語) や Twitter (@despos、英語) でソフトウェアに関するビジョンを紹介しています。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Jon Arne Saeteras に心より感謝いたします。