June 2016
Volume 31 Number 6
Cutting Edge - 履歴 CRUD の構築 (第 2 部)
Dino Esposito | June 2016
概念上、履歴 CRUD (作成 (Create)、読み取り (Read)、更新 (Update)、削除 (Delete)) とは、従来の CRUD にパラメーター (日付) を追加して拡張したものです。履歴 CRUD により、データベースとの間でレコードを追加、更新、削除でき、特定時点のデータベース状態を照会できます。履歴 CRUD は、ビジネス インテリジェンス分析や高度なレポート機能を目的とする組み込みのインフラストラクチャをアプリケーションに提供します。
先月号のコラム (msdn.com/magazine/mt703431) では、履歴 CRUD システムの理論的な基礎を取り上げました。今回は、実践的デモを紹介します。
サンプル シナリオ
今回説明する目的に合わせて、簡単な予約システムを考えます。たとえば、ある企業ではこのシステムを社内に導入して、社内会議室の予約に使用するとします。いずれにせよ、このようなソフトウェアは、会議室の時間枠を予約するために新しいレコードを作成するプレーンな CRUD です。会議時間帯を変える場合は同じレコードを更新し、会議をキャンセルする場合はそのレコードを削除します。
このような予約システムを標準の CRUD でコーディングすると、システムの最終状態は分かりますが、会議の更新や削除についての情報はすべて失われます。こうした情報が失われることは本当に問題なのでしょうか。答えは状況によって変わります。ビジネス現場での会議室の予約について単純に考えれば、そのような情報が失われてもほとんど問題はありません。ですが、社員の全体的なパフォーマンスを向上する方法を模索しているとしたらどうでしょう。履歴 CRUD によってレコードの更新と削除を追跡すると、多くの会議が変更されたりキャンセルされていることがわかり、非効率なプロセスや不適切な姿勢の兆候が見つかるかもしれません。
図 1 は、会議室予約システムの実際の UI を示しています。基盤となるデータベースは、1 組のリンク テーブル (Rooms とBookings) を含む SQL Server データベースです。
図 1 予約システムのフロントエンド UI
サンプル アプリケーションは ASP.NET MVC アプリケーションとして設計しています。ユーザーがクリックして要求を行うと、コントローラー メソッドが作動して、送られた情報を処理します。以下のコード スニペットを見れば、サーバー側で要求を処理するコードの考え方が明らかになります。
[HttpPost]
public ActionResult Add(RoomRequest room)
{
service.AddBooking(room);
return RedirectToAction("index", "home");
}
このメソッドは BookingController クラスに含まれ、実際の機能に体系化する作業を、挿入される Worker サービス クラスにデリゲートします。このメソッドの実装で興味深い側面は、このメソッドが予約を作成した後に 図 1 のフロント ページにリダイレクトされる点です。予約の追加操作の結果として明示的に作成されるビューはありません。これはコマンド クエリ責務分離 (CQRS) アーキテクチャを採用する二次効果です。予約の追加コマンドがバックエンドに送られ、システムの状態が変更されて、終了します。サンプル アプリケーションでコマンドの送信に AJAX を使用する場合、何も更新する必要はなく、コマンドは UI へのリンクを可視にしないスタンドアローン操作になります。
従来の CRUD と履歴 CRUD の根本的な違いは、履歴 CRUD がシステムの状態を変更する操作を起動時からすべて記録している点にあります。履歴 CRUD を計画する場合は、ビジネス上の各操作をシステムに与えるコマンドと考え、こうしたコマンドを追跡するメカニズムを考えます。各コマンドがシステムの状態を変更し、履歴 CRUD が変更後のシステムの状態を追跡します。変更後の状態はイベントとしてログに記録されます。イベントは、発生した事象の短い説明で、これを変更することはできません。イベントのリストを取得したら、そのリストを基盤としてデータの複数のプロジェクションを作成できます。最も一般的なのは、関与したビジネス エンティティの現在状態です。
アプリケーションでは、イベントはユーザー コマンドの実行によって直接生成されるか、それ以外のコマンドや外部入力によって間接的に生成されます。今回のサンプル シナリオでは、ユーザーはボタンをクリックして予約要求を送信すると想定しています。
コマンドの処理
以下のコードは、アプリケーションのコントローラーでの AddBooking メソッド実装例です。
public void AddBooking(RoomRequest request)
{
var command = new RequestBookingCommand(request);
var saga = new BookingSaga();
var response = saga.AddBooking(command);
// Do something based on the outcome of the command
}
RoomRequest クラスは、ASP.NET MVC バインド層によって設定されるプレーンなデータ転送オブジェクトで、その内容は送信されたデータから設定されます。RequestBookingCommand クラスが代わりに、コマンドの実行に必要な入力パラメーターを格納します。今回のサンプル シナリオでは、この 2 つのクラスはほぼ同じです。このコマンドはどのように処理すればよいでしょう。図 2 は、コマンドを処理する 3 つの主要手順を示しています。
図 2 コマンドを処理する一連の主要手順
ハンドラーは、コマンドを受け取って処理するコンポーネントです。ハンドラーは、Worker サービスのコード内から直接インメモリ呼び出しを行うことで起動されます。または、以下に示すように、中間にバスを用意することができます。
public void AddBooking(RoomRequest request)
{
var command = new RequestBookingCommand(request);
// Place the command on the bus for
// registered components to pick it up
BookingApplication.Bus.Send(command);
}
バスは 2 つのメリットをもたらす可能性があります。1 つは、複数のハンドラーが同じコマンドに関心を持つ可能性のあるシナリオを簡単に処理できることです。もう 1 つは、信頼性の高いメッセージ ツールになるようにバスを構成できる点です。その結果、長時間のメッセージの配信が確保され、接続に関する潜在的な問題を解決できます。または、バスを単にコマンドを記録する機能を提供するコンポーネントにすることもできます。
ハンドラーは、同じ要求で開始と終了を行う簡単な 1 回限りのコンポーネントにすることも、完了するまでに数時間から数日かかる長時間実行されるワークフローにすることも、ある時点で一時停止してユーザーの承認を得るまで待機することもできます。簡単な 1 回限りのタスクを実行するハンドラー以外のハンドラーは、「サガ」と呼ばれることがよくあります。
一般に、スケーラビリティや信頼性に関して具体的な要件がある場合は、バスやキューを使用します。従来の CRUD の代わりに履歴 CRUD を作成することを考えているのであれば、バスを使用する必要はほぼありません。バスを使用するかどうかに関わらず、コマンドはある時点で、1 回限りのハンドラーまたは長時間実行されるハンドラーにアクセスします。そのハンドラーが、想定しているタスクをすべて実行するものとします。ほとんどのタスクは、データベースでの主要操作で構成されます。
コマンドの記録
従来の CRUD では、データベースに情報を書き込むことは、渡された値から組み立てたレコードを追加することを意味します。履歴 CRUD の観点では、新しく追加するレコードは、予約の作成イベントを表します。予約の作成イベントは、イベントの一意 ID、タイムスタンプ、名前、イベント固有の引数のリストを含む、独立した変更不可の情報です。通常、作成イベントの引数は、従来の Bookings テーブルに新しく追加される Booking レコードに関して入力されるすべての列を含みます。更新イベントの引数には、実際に更新されるフィールドしか含みません。そのため、更新イベントのコンテンツはすべて同じになるとは限りません。削除イベントの引数は、予約を一意に特定する値だけを含みます。
履歴 CRUD のすべての操作は次の 2 つの手順で構成されます。
- イベントと関連データを記録する。
- システムの現在状態を即座に照会できるようにする。
この方法によって、システムの現在状態が常に入手可能で最新状態になり、その状態に至るまでのすべての操作も、その後の分析に利用できるようになります。「システムの現在状態」は、従来の CRUD システムでは唯一確認できる状態です。簡単な CRUD システムのコンテキストで効率を高めるには、イベントを記録してシステム状態を更新する手順を、同じトランザクション内で同期を取って実行します (図 3 参照)。
図 3 イベントの記録とシステムの更新
using (var tx = new TransactionScope())
{
// Create the "regular" booking in the Bookings table
var booking = _bookingRepository.AddBooking(
command.RoomId, ...);
if (booking == null)
{
tx.Dispose();
return CommandResponse.Fail;
}
// Track that a booking was created
var eventToLog = command.ToEvent(booking.Id);
eventRepository.Store(eventToLog);
tx.Complete();
return CommandResponse.Ok;
}
現状では、予約レコードを追加、編集、または削除するたびに、予約のリスト全体が最新状態に維持されると同時に、現在状態に至るまでのイベントの正確なシーケンスも認識されています。図 4 は、サンプル シナリオに関係する 2 つの SQL Server テーブルと、挿入と更新を行った後のテーブルのコンテンツを示しています。
図 4 並べて示した Bookings テーブルと LoggedEvents テーブル
Bookings テーブルは、システム内にある個々の予約をすべてリストし、個々の予約の現在状態を返します。LoggedEvents テーブルは、さまざまな予約に関するすべてのイベントを、記録された順番にリストします。たとえば、予約 54 はある日付に作成され、数日後に変更されています。上記の例の Cargo 列は、実行されたコマンドの JSON シリアル化済みストリームを格納しています。
ログに記録されたイベントの UI での使用
今度は、承認を受けたユーザーが、保留中の予約の詳細を確認できるようにします。ユーザーは、おそらく、カレンダーのリストまたは時間に基づくクエリを使用して予約を行います。どちらの方法でも、予約日時、会議時間、参加者など、予約の基本情報は既に把握されていて、それについての詳細なビューはほとんど使用されません。しかし、予約の履歴全体を表示する場合は、そのような詳細ビューが役に立ちます (図 5 参照)。
図 5 記録されたイベントの UI での使用
記録されたイベントすべてを読み取ることで、同じ集計エンティティ (Booking #54) のリストを含むビュー モデルを構築できます。サンプル アプリケーションでは、ユーザーがクリックして予約の詳細を表示すると、モーダル ポップアップが表示され、何らかの JSON がバックグラウンドでダウンロードされます。JSON を返すエンドポイントを以下に示します。
public JsonResult BookingHistory(int id)
{
var history = _service.History(id);
var dto = history.ToJavaScriptSlotHistory();
return Json(dto, JsonRequestBehavior.AllowGet);
}
Worker サービスの History メソッドが、ここでの作業の大半を実行します。この作業の重要な部分は、指定した予約 ID に関連するイベントすべてを照会することです。
var events = new EventRepository().All(aggregateId);
foreach (var e in events)
{
var slot = new SlotInfo();
switch (e.Action)
{
:
}
history.Changelist.Add(slot);
}
記録されたイベント全体をループ処理しながら、呼び出し元へ返すデータ転送オブジェクトに適切なオブジェクトを追加していきます。ToJavaScriptSlotHistory で実行する変換により、図 5 に示した形式で、2 つの連続する状態の間の差分をすばやく簡単に表示できるようになります。
CRUD 内でイベントを記録するだけでも UI にこのようなすばらしい機能強化を施すことができます。同時に、システム内でそれまでに行われてきたことをすべて把握できるようになり、そのデータを処理すれば、ある時点で必要なデータのカスタム プロジェクションを抽出できるというところに最大の価値があります。たとえば、更新の統計を作成すれば、アナリストは、予約後の更新や削除が多く、会議室を必要とするプロセス全体が社内で機能していないと結論付けることができます。さらに、特定の日付までに記録されたイベントを照会し、その後の状態の推移を予測することによって、その日付の予約状況を簡単に追跡できます。端的に言えば、履歴 CRUD は、アプリケーションにこれまでにない新しい世界を開きます。
まとめ
履歴 CRUD は、プレーンな CRUD アプリケーションをよりスマートに進化させる方法にすぎません。しかし、このシリーズでは、CQRS、イベント ソーシング、バスとキュー、メッセージベースのビジネス ロジックなど、多くの可能性を秘めた専門用語やパターンを取り上げました。このシーリーズが役に立ったと感じた方は、2015 年 7 月のコラム (msdn.com/magazine/mt238399) と 2015 年 8 月のコラム (msdn.com/magazine/mt185569) をお読みいただくことをお勧めします。ここでの例を踏まえると、このコラムがさらに興味深く感じられると思います。
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 に心より感謝いたします。