レイヤとデータからみるDCIアーキテクチャの実装

DCIアーキテクチャに従ってCRUDアプリケーションを実装する際のポイントを整理する。

導入

Trygve Reenskaug氏によって提唱され、主にJames O. Coplien氏によって理論的な確立と普及が進められているDCIアーキテクチャですが、まだ実装事例はそれほど多くありません。このブログではこれまで「割り勘」を題材にDCIアーキテクチャに基づくCRUDアプリケーションを実装しつつ、アプリケーションの全体像について考察してきました。今回は割り勘アプリケーションをふりかえりつつ、もう少し実装の詳細について考えていきます。

アプリケーションレイヤ

アプリケーションレイヤとはDDDにおいて説明されているレイヤの1つで、ドメインレイヤを薄くラップし、タスクに応じて適切なドメインロジックの呼び出しを行うという責務を与えられています。

アクションクラス

具体例として、割り勘アプリケーションにおいて各参加者の割り当てを計算するロジックのシーケンス図を見てみましょう。

ここでは、Actionがアプリケーションレイヤのコンポーネントとなっています。見ていただければ分かる通り、コンテキストのメソッドを呼び出すことでドメインロジックを実行した上で、結果を画面に表示するための変換ロジックを走らせています。ソースコードを掲載します。

   /**
     * Execute calculation.
     * 
     * Execute adjust in Accounting context and create paymentListItems from party.
     * Result of this method is not committed.
     * 
     * @param partyId ID of Party
     * @param slopeId ID of Slope to be used
     * @return PaymentListItems reflecting allots in Party entity 
     */
    def calculate(partyId:String, slopeId:String):java.util.List[PaymentListItem] = {
        
        val accounting:Accounting = new Accounting(partyId)
        
        accounting.adjustBy(slopeId)
        
        createPaymentListItemsOfParty(accounting.party)
    }

    private def createPaymentListItemsOfParty(party:Party):java.util.List[PaymentListItem] = {
        val paymentListItems = new ArrayList[PaymentListItem]()
        party.participants.values.foreach { participant =>
            val paymentListItem:PaymentListItem = convertToPaymentListItem(participant, party.paymentOf(participant.userName))
            paymentListItems.add(paymentListItem)
        }
        paymentListItems
    }

この場合は、ドメインモデルとビューモデルの変換処理がアクションクラスの責務として与えられています。付け加えれば、まだ実装されていないバリデーションロジックも、ドメインレイヤへの入力を保証するタイプのものはここに実装されることになります。


ドメインモデルがユーザのメンタルモデルと整合していたとしても、Viewとして表示するためにはそれを操作しやすいように切り取る必要があります。これは3次元の立体を切断して2次元の平面を見せるようなものと考えると分かりやすいかもしれません。そういった「見せ方」はドメインモデルの変化とは別の水準で変化する可能性がある部分であるため、レイヤとしても分けておくべきでしょう。

トランザクションスクリプト

ドメインモデルを説明するためのシンプルなサンプル」には、実はドメインロジックと呼べるようなものがほとんど存在せず、「これならトランザクションスクリプトで良いのでは」ということになりがちというのは、普遍的な悩みなのかもしれません。「割り勘アプリケーション」にも同じパターンに陥っており、「傾斜付き割り勘の計算」を除けば、ドメインロジックと呼べるようなものはありません。こういう場合には、どこまでロールオブジェクトを作るべきなのでしょうか。


割り勘アプリケーションでは、暫定的にCQRSのアイデアを借り、検索に限ってはActionクラスから直接リポジトリクラスにアクセス可能、としています。しかし、これだけではまだすっきりしておらず、Actionから直接データオブジェクトを操作しても良いのではないかと思われる処理も少なくありません。逆に、分析系の処理であれば検索にビジネスロジックが入ってくる可能性も十分にあり得ます。割り切り方としては「データオブジェクトに準備された基本的なAPIを操作するだけで事が足りるならばトランザクションスクリプトを貫き、なんらかのビジネスロジックが発生したタイミングでロールとコンテキストを作る」という方針の方が適しているのかもしれません。この点においても、ドメイン層とのファサードとしてアプリケーションレイヤを準備しておく事には意味があります。

データ設計

ここでは、DCIで実装する場合のデータ設計方針について考えたいと思います。まずは、割り勘アプリケーションで使っているデータのクラス図を示します。

エンティティとバリューオブジェクト

DDDの用語に従えば、図中青く塗られているのがエンティティ、グレーで塗られているのがバリューオブジェクトになります。DDD流の実装であれば、「隠れている概念をバリューオブジェクトとして抽出し、そこにふるまいをもたせる」ということになりますが、DCIのパラダイムではふるまいをロールクラスに集中させるため、バリューオブジェクトにふるまいは持たせません。


ならば、例えばUserNameのように、わざわざフィールドが1つだけのクラスを作る必要があるのか、という疑問が浮かびますが、ふるまいを持たせなかったとしても、ソースコードの可読性が格段に向上するというメリットがあります。バリューオブジェクトを作る前後のコードを比較してみましょう。

class Party(val id:String) {
	
	var name:String = null
	
	var schedule:Date = null
	
	var location:String = null
	
	var sum:Int = 0
	
	var participants = Map[String, Participant]() // userName, Participant
	
	var allots = Map[String, Allot]() // userName, Allot

participantsの宣言箇所では、キーの型しか見えないため、「String」と言われても何か分かりません。それを補うためにコメントを書いているのですが、「コメントを書いて補うならば、リファクタリングせよ」です。

@serializable
class Party(val id:String) {

    var info:PartyInformation = _
    
    var sum:Int = 0

    private var _participants = Map[UserName, Participant]()

    private var _allots = Map[UserName, Allot]()

この違いはクライアントコードにおいてさらに顕著に現れます。

    planning.addPerticipant("A", "a")
    planning.addPerticipant("B", "b")
    planning.commit

バリューオブジェクトを作らなかった場合、ここだけ見ると意味が理解できません。planning.addPerticipant("太郎", "部長")などと書くべきなのかもしれませんが、任意の値が持つ意味でコードを解釈させることが危険であることには変わりありません。バリューオブジェクトを使えばこうなります。

    planning.addPerticipant(UserName("A"), Role("a"))
    planning.addPerticipant(UserName("B"), Role("b"))
    planning.commit

planning.addPerticipant(UserName("太郎"), Role("部長"))の方はやや冗長に見えるかもしれませんが、解釈の安定感は格段に向上しています。


ここから分かる通り、DCIを採用したとしてもデータの設計指針には特別な変化はありません。メンタルモデルに忠実に、概念を抽出しながらモデリングしていけば良いことになります。

集約ルートとロールオブジェクト

DCIではデータはビジネスロジックに関わるようなふるまいは持たず、その役割はロールオブジェクトが担います。そこで問題となるのが、「どのデータオブジェクトに対してロールオブジェクトを注入すれば良いのか」です。これについてはいろいろやり方はあると思いますが、そこで注意しなければいけないポイントがあります。

  1. ロールを注入しているデータオブジェクトの構造が変わると、実装へのダメージが大きい
  2. ロールを細分化しすぎてしまうと、アルゴリズムを集約するというDCIのそもそもの目的が失われてしまう。

DCIアーキテクチャがシステムを「構造」と「ふるまい」とに分けたのは、「ふるまい」という変化しやすい部分を「構造」という安定した部分から切り離すためでした。したがって、ロールの注入は、ある程度大きな粒度を持ち、安定したポイントに対して行う必要があるのです。


割り勘アプリケーションでは、ここでも再びDDDを参考にし、「集約ルート(Aggregate Root)」という概念を借りています。集約ルートに関する説明を一部引用します。

Cluster the ENTITES and VALUE OBJECTS into AGGREGATES and define boundaries around each. Choose one ENTITY to be the root of each AGGREGATE, and control all access to the objects inside the boundary through the root.


エンティティとバリューオブジェクトを集約(AGGREGATE)内に集め、その周りに境界を定義してください。各集約に対するルートにはエンティティを1つ選び、境界内のオブジェクトに対するアクセスはすべてルートを通じてコントロールしてください。

集約ルートとなるエンティティは、そのドメインにおける主要な概念であり、リファクタリングによってモデルに多少の変化があったとしても、ルートにまで影響をおよぼすものはそれほどないと考えられます。また、境界内のオブジェクトのコントロールを仲介するという意味でも、ロールの注入先としては適しています。したがって、「ロールの注入は集約ルートに対して行う」ということを1つの指針にすることができます。(今回の例で言えば、「飲み会(Party)」と「傾斜(Slope)」が集約ルートになっています。)

まとめ

DCIアーキテクチャに基づいたWebアプリケーションを作る上での実装上の考慮点について考察してきました。気がついた方もいらっしゃると思いますが、実は「DCIだからやらなければいけないこと」は特になく、アプリケーションを作成する上でどうレイヤを切るか、またどうやってデータを設計するのかという話がベースになっています。「Scalaを使う」あるいは「Traitを使う」という新しさとは別の次元で、基本は基本であり続けるということですね。


割り勘アプリケーションはこちらで公開しています。

Greg Young流CQRS - Mark Nijhof

この記事はMark Nijhof氏のブログ記事「CQRS à la Greg Young」を氏の許可を得て翻訳したものです。(原文公開日:2009/11/11)




この記事は以前のブログである"blog.fohjin.com"にて公開していたものです。


以前、2日間の講習を受けた時に、ビールを飲みながらGreg Young氏とドメイン駆動設計について語るという幸運に恵まれたことがあります。その時の話題は専ら、コマンドクエリ責務分離(CQRS:Command and Query Responsibility Segregation)パターンに関するものでした。Gregは、Eric Evans氏が著作において説明したドメイン駆動設計を受け継ぎ、主に技術的な実装を進化させています。コマンドクエリ分離(CQS)は元々Bertrand Meyer氏によって考案されたもので、オブジェクトのレベルで適用されていました。

CQS: 「あらゆるメソッドは、アクションを実行するコマンドか、呼び出し元にデータを返すクエリかのいずれかであって、両方を行ってはならない。これは、質問をすることで回答を変化させてはならないということだ。

-Bertrand Meyer

これに対して、Gregは同様の原則を用いつつ、それをシステムのアーキテクチャ全体に適用し、システムの更新処理("Command")を参照処理("Query")から明確に分離したのです。このうち更新処理は、私たちがすでに「ドメイン」として知っているもので、システムの価値を生むすべてのふるまいを含んでいます。参照処理は特定のレポートを行う必要性に特化したものです。例えば、ユーザがドメインのふるまいを実行できるようにするアプリケーションの画面を考えてみて下さい。このような伝統的なレポートは、データベースの参照によって実現されてきました。


詳細に踏み込む前に、アーキテクチャ全体について簡単に見て行きましょう。

Command and Query Responsibility Segregation (CQRS) - Overview Command and Query Responsibility Segregation (CQRS) – 概要

Visioを使って書いていないことを大目に見て下さい。今夜はこれ以上別のレイヤで複雑なことに取り組みたくなかったのです。最低限、ラベルを打ち込んでおきましたので、読めるようにはなっています。このアーキテクチャについては4つのパートに分けて議論していきます。この順序はあなた方がこのアーキテクチャについて自然に考える順序だと思っています。最終的には原則全体が比較的シンプルであることが分かるでしょう。本当ですよ。

Command and Query Responsibility Segregation (CQRS) - Division Command and Query Responsibility Segregation (CQRS) – パート分割

  1. クエリ
  2. コマンド
  3. 内部イベント
  4. 外部イベント(公開)

この種のアーキテクチャをより良く理解するために、私はサンプルアプリケーションを構築することにしました。このサンプルは既に野に放たれていて(つまりYahooのDDDグループのことですが)、ここで見ることができます:http://github.com/MarkNijhof/Fohjin

クエリ(レポーティング)

最初に議論したいのは、システムにおけるレポートの必要性です。Gregはシステムからデータを取得する必要性はどれもレポートの必要性だと定義しています。ここにはユーザが意思決定をする際に用いる様々なアプリケーションの画面も含まれます。一見奇妙な定義に思えますが、深く考えれば考えるほど、この意味が分かってきます。このデータはユーザ(あるいは他のシステム)に対して、ユーザがおかれた特定のコンテキストにおけるシステムの現在の状態を示すことで、ユーザがなんらかの意思決定を行い、ドメインのふるまいを実行できるようにするものです。


これらのレポートは、レポートの利用者、すなわちドメインの状態を表すデータを見る人によって直接更新されることはなく、それを更新する責務を負うのはドメインです。したがって、この部分で実際に行うのは、システムの現在の状態について、それを必要とする人にそれが誰あるいは何であれレポートすることだけです。
アプリケーションによって特定の画面に表示するためにデータが要求された場合、これはクエリレイヤに対する単一の呼び出しによって実現され、戻り値としては必要なデータをすべて含んだDTOが返されます。このようにデータの使用方法が特定されるので、システムのニーズに応じて並び替えないしグルーピングを行うことには意味があります。したがって、単一の画面ないし特定のクライアントアプリケーションにおける使用法を反映させるような単一のテーブルを作成するために、データの非正規化を行うことになります。なぜなら、データは通常ドメインのふるまいが実行されるよりも参照されることの方が多いのであり、ここを最適化することでシステムの体感性能を向上させることができるのです。


ここで、データベースからの読み取りを容易にするために、NHibernateのようなORMを使おうと思うかもしれません。しかし、これでは適切なORMが持つ機能のごく一部しか使っていないということを考慮すれば、このようにする必要はまったくありません。DTOから直接SQL文生成するためにリフレクションを利用することをGregが提案したように(リフレクションとCoCを利用することでさらにシンプルになります)、Linq2Sqlを試してみる方がよい考えかもしれません。これはあなた次第で、特定のシナリオと何を良いと思うかに依存します。


このアーキテクチャを説明するのに作ったサンプルでは、私はDTOのリフレクションを使うことにしました。これは私が強調したかったのがCQRSの実装であって、ORMのそれではなかったからです。


より伝統的なレポートが必要とするのは、独自のデータベーススキーマであり、そのニーズに合わせて最適化されたデータです。そうなると最終的には、大量のデータが重複することになりますが、それは構わないのです。別々のデータベース上でデータを更新する責務を追ったプロセスにより、この更新が正しい仕方で起こることを保証します。これについては後半で議論することにします。

コマンド(ドメイン上のふるまいを実行する)

まずは、DTOを受け取った後で通常何が起こるのかを考えましょう。ユーザはデータを変更し、それをDTOに戻します。その際、このDTOはバックエンドに送り返され、エンティティに変換された上で、その変更がデータベース上に永続化されることはORMによって保証されます。


この結果、きわめて重要な情報が失われることになります。すなわち、なぜその変更が起きたのかということがです。データを変更した際にユーザが持っていた意図が完全に見失われてしまうこと、それがGregのCQRS実装によって解決される問題です。


名前が示す通り、CQRSはコマンドを利用します。これらのコマンドはクライアントアプリケーション上に生成され、ドメインレイヤに送信されます。例を見てみましょう:銀行の顧客がオフィスに入り、受付にいる人に住所を変更したいと声をかけます。そこで単に顧客情報を開いて住所を直接変更するのではなく、銀行員はまず質問をします。「どうして住所を変更したいのでしょうか?」ほとんどの場合、引越したという答えが返ってくるでしょう。しかし、存在しない住所だったとか、手紙がすべて階下の住人に届いてしまったということも考えられます。つまり顧客の住所を更新するのには全く異なる理由が2つ考えられるのです。なぜこれが重要なのでしょうか?確かにこの例は若干馬鹿げているかもしれません。しかし、顧客が引っ越した後で競合他社に行くケースがどのくらいあるのか知りたいと想定して下さい。この銀行に対して顧客はどの程度忠誠心を持っているのか、そしてXマイル離れた場所に引っ越した後も特定の情報を送り続けるべきなのか?まさにこうした情報が当初のやり方では失われてしまうのです。しかしコマンドとイベント(イベントについては後述)を利用することで、このアクションの当初の意図を保持することができます。さて、質問を受けた顧客が引越をしたのだと答えたところで、銀行員はアプリケーション上で「引越による」を選択し、住所だけを変更することができるようになります。保存をクリックした際に、変更された住所だけを保持したCustomerMovedCommandが生成され、ドメインへと送信されます。


こういったコマンドを利用することで得られるメリットがもう1つあります。それはこれらのコマンドを使うと、システムの構築ないし運用時に顧客とのコミュニケーションが容易になるということです。これは顧客が自分たちが行いたいことを説明するのに、この種のふるまいを使うことが多いからです。Gregも時代は変わったと考えていますが("Our grand failure")、このことは顧客が自分たちのドメイン言語を語る時にまさに当てはまります。こういったコマンドを利用することで、私たちは、コードの中でも同じ言語を話すことができるようになります。


こうしたことはまさにドメイン駆動設計が語っていることです。顧客情報を更新するといった技術的なことを実行するのではなく、顧客の引越といったユーザが使用しているプロセスをコードの中で実際に記述するのです。

コマンド
namespace Fohjin.DDD.Commands
{
    [Serializable]
    public class ClientIsMovingCommand : Command
    {
        public string Street { get; private set; }
        public string StreetNumber { get; private set; }
        public string PostalCode { get; private set; }
        public string City { get; private set; }

        public ClientIsMovingCommand(Guid id, string street, string streetNumber, string postalCode, string city) : base(id)
        {
            Street = street;
            StreetNumber = streetNumber;
            PostalCode = postalCode;
            City = city;
        }
    }
}

これらのコマンドはすべてコマンドバスに送信され、コマンドバスは各コマンドを1つないし複数のコマンドハンドラに委譲します。これが意味しているのは、ドメインに対するエントリポイントが1つしかなく、それはバスだということです。これらコマンドハンドラが持つ責務は、ドメイン上で適切なふるまいを実行するというものです。これらコマンドハンドラのほぼすべてに対して、集約ルート("Aggregate Root")をロードできるようにリポジトリがインジェクトされ、その集約ルートに対する適切なふるまいが実行されます。通常、1つのコマンドハンドラに対して必要な集約ルートは1つです。あとでリポジトリについても詳細に見ておきましょう。通常のDDDのリポジトリとは異なるからです。

コマンドハンドラ
namespace Fohjin.DDD.CommandHandlers
{
    public class ClientIsMovingCommandHandler : ICommandHandler<ClientIsMovingCommand>
    {
        private readonly IDomainRepository _repository;

        public ClientIsMovingCommandHandler(IDomainRepository repository)
        {
            _repository = repository;
        }

        public void Execute(ClientIsMovingCommand compensatingCommand)
        {
            var client = _repository.GetById<Client>(compensatingCommand.Id);

            client.ClientMoved(new Address(compensatingCommand.Street, compensatingCommand.StreetNumber, compensatingCommand.PostalCode, compensatingCommand.City));
        }
    }
}

ご覧の通り、コマンドハンドラに責務は1つしかなく、それは適切なドメインのふるまいを実行することで特定のコマンドを処理するというものです。コマンドハンドラはドメインロジック以外、何も行ってはいけません。必要があれば、ロジックはサービスに移されるべきです。サンプルコード中の例としては、収入の振替があります。これについては後述します。

内部イベント(意図をとらえる)

ついに実際のドメインに到着しました。クライアントは私たちのドメインのあるビューを要求し、適切なレポートDTOを受け取り、意思決定を行った上で公開されたコマンドを呼び出します。所定のコマンドハンドラが、そこで正しい集約ルートをロードし、適切なドメインのふるまいを実行します。次になにがあるでしょう?


ここで行うべきは、ドメインのふるまいを、その結果として生じる状態("state")の変化から切り離すということです。なお、ここでいう変化には外部のふるまいをキックすることも含まれます。こうしたことを今行っているやり方とはずいぶん異なると思います。まず入力チェックを行い、実行すべきことを実行し、内部の状態を設定することなく、また外部のふるまいをキックすることのないやり方とは異なるということです(確かに後半部はあまり考えられることがないかもしれません。状態がここでのポイントです)。こういった状態の変更を、生成した内部変数に直接書き込む代わりに、イベントを生成して内部的にキックするのです。ふるまいに与えられたメソッド名と同様、このイベントもドメインユビキタス言語を記述するものとなっているべきです。そのとき、イベントはドメインの集約ルートの内部で処理され、正しい値に対して内部の状態を設定します。イベントハンドラが状態を設定するということ以外に一切のロジックを実行しないということを忘れないで下さい。このロジックはドメインメソッドの内部にあるのです。

ドメインのふるまい
public void ClientMoved(Address newAddress)
{
    IsClientCreated();

    Apply(new ClientMovedEvent(newAddress.Street, newAddress.StreetNumber, newAddress.PostalCode, newAddress.City));
}
 
private void IsClientCreated()
{
    if (Id == Guid.Empty)
        throw new NonExistingClientException("The Client is not created and no opperations can be executed on it");
}
ドメインイベント
namespace Fohjin.DDD.Events.Client
{
    [Serializable]
    public class ClientMovedEvent : DomainEvent
    {
        public string Street { get; private set; }
        public string StreetNumber { get; private set; }
        public string PostalCode { get; private set; }
        public string City { get; private set; }
 
        public ClientMovedEvent(string street, string streetNumber, string postalCode, string city)
        {
            Street = street;
            StreetNumber = streetNumber;
            PostalCode = postalCode;
            City = city;
        }
    }
}
内部ドメインイベントハンドラ
private void onNewClientMoved(ClientMovedEvent clientMovedEvent)
{
    _address = new Address(clientMovedEvent.Street, clientMovedEvent.StreetNumber, clientMovedEvent.PostalCode, clientMovedEvent.City);
}

こういったイベントが必要になるのは、これが今や永続化戦略の一部になっているからです。これはつまり集約ルートに関して永続化すべき情報は生成されたイベントだけだということです。状態のあらゆる変更が何らかのイベントによってキックされ、内部イベントハンドラが正しい状態を設定するということ以外にロジックを持たなかったとすれば(これはイベントにおいてデータから他の情報を取り出さないということを意味します)、私たちにできるのは履歴にあるイベントをすべてロードし、集約ルートに対して内部的にリプレイすることで、最初と全く同じ状態に戻すということです。これはテープの再生と同じです。


特筆すべきは、こうしたイベントが書き込み限定のものであり、イベントを追加したり、変更したり、削除したりすることができないということです。もし誤ったイベントを生成するバグが出てしまった時に、それを修正するには、バグの結果を修正するような埋め合わせイベントを生成するほかありません。もちろん、バグを修正したいとも思うでしょう。このようなやり方により、いつバグが修正されたか、そしてバグの結果が正されたかを追跡するのです。


このようなアーキテクチャを採用することにより、本来の意図を見失ってしまうという問題は基本的に解決されました。発生したイベントはすべて保持されており、こういったイベントは意図が明確だからです。興味深い点がもう1つあります。それは監査ログを自由に取得できるということです。イベントがなければ状態が変更されることがなく、イベントは保存され、集約ルートを構築するのに使用されるので、相互に同期が取れていることが保証されます。


ドメインリポジトリ

先ほど、ドメインリポジトリは通常DDDを実践する場合と比べて完全に異なることになるだろうと述べました。普通は、ドメインからあらゆる種類の情報を引き出せるようにする、要件に特化したリポジトリを作ることになります。しかしGregのCQRS実装を利用する場合、ドメインは完全に書き込み専用となるため、リポジトリはIDによって集約ルートを取得できればよく、また生成されたイベントを保存できなければなりません。同じくドメインと永続化レイヤとの間にあるインピーダンス不整合も完全に回避されます。

ドメインリポジトリコントラクト
namespace Fohjin.DDD.EventStore
{
    public interface IDomainRepository 
    {
        TAggregate GetById<TAggregate>(Guid id)
            where TAggregate : class, IOrginator, IAggregateRootEventProvider, new();

        void Add<TAggregate>(TAggregate aggregateRoot)
            where TAggregate : class, IOrginator, IAggregateRootEventProvider, new();
    }
}

一方、レポート用リポジトリはDDDの伝統的なリポジトリに近いものになります。


ここで、もし100,000イベントが存在し、集約ルートをロードするたびにリプレイする必要があるとすれば、システムは恐ろしく遅くなってしまうでしょう。この問題に対処するには、メメントパターンを使い、一定数のイベントがあるたびに集約ルートの内部的な状態についてスナップショットを取ることが考えられます。リポジトリはまずスナップショットを要求し、集約ルート内にロードし、スナップショットの後で発生したすべてのイベントを要求して、本来の状態に戻します。これは最適化のテクニックにすぎないため、スナップショット以前のイベントを消してはいけません。それではこのアーキテクチャの目的が失われてしまいます。(※訳註1)


データマイニング

すべてのイベントを保存しておくことについて、興味深い事実が他にもあります。こういったイベントを後でリプレイし、重要な業務情報をそこから引き出すことができるのです。しかもこの情報はシステムの状態から取得できるのであり、特殊なログを組み込んで信頼できる情報を得るために数ヶ月待つ必要はありません。

外部イベント(公開、外部に知らせる)

ついにこの解説の4つ目のパートにたどり着きました(もう一言自分のために。「やれやれ」)。さて、ここでは何が起こるでしょうか。ここまででできるようになったこととしては、ドメインの状態を読み取ることができ、ふるまいを実行してドメインの内部の状態を更新することができます。明らかに足りないのは、レポート用データベースにドメインの現在の状態を設定する方法です。これは、内部のドメインイベントを外部に公開することによって行われます。そこで、こういったイベントを検知し、レポート用データベースと同期するイベントハンドラが登場します。ここでORMを使うこともできますが、必要なSQL文を生成し、実行するのもきわめて容易です。


GregはこれらのSQL文をキャッシュするにあたり、実に優れた方法を説明しています。ある単独のバッチ処理の中に束ね、そのバッチを数秒おきに実行するか、(これが面白いところですが)参照リクエストが送信された時に実行すると言っています。参照リクエストが送信された時、このSQL文がバッチに加えられ、すべて実行されます。その際、システムのこの場所で得られる最新のデータに対し、参照リクエストがアクセスできることが保証されます。詳細については後述します。


ドメインリポジトリはイベントを公開するという責務を持ちます。これは通常であれば、単一のトランザクション内でイベント格納箇所にイベントを格納することで実施されます。


イベントはまた異なる集約ルート間でのコミュニケーションにも使われます。サンプルでは、ある口座から別の口座への取引を用いています。ここでは資金を別の口座に移動するイベントを生成していますが、このイベントはまた、現在の口座の残高も減らします。後になって、イベントハンドラがレポート用データベースに対して同じ変更を加えます。このもう1つのイベントハンドラは実際には、取引情報をサービスに送信します。このサービスは送金対象口座がローカルにある口座なのかどうかを調べ、そうでなければ送金情報を別の銀行に送信します(サンプルでは、ここでいう別の銀行も実際には同じですが、別のルートを辿ります)。しかし、送金が内部の口座に対して行われると想定してください。この場合、サービスはコマンドバス内にある送金受信コマンドを公開し、その後のプロセスはすべて別の集約ルートに対して行われます。この場合であれば、コマンドはGUIからではなく、システムの別の部分からキックされることになります。


他の銀行が資金を受け取るというこのシナリオに関しては、興味深い事実がもう1つあります。対象となる口座を識別するにあたって送金情報に含まれているのは口座番号であって集約ルートのIDではありませんので(外部システムがこの集約ルートIDを知ることを期待すべきではありません。周知の通り自然キーとしても生成できるからです)、資金受取サービスはまずレポート用データベースに対してクエリを実行し、口座番号が対象口座と同じである口座DTOを取得します。このクエリが成功した際には、口座DTOから取得したIDを利用して公開されるコマンドに設定します。


しかし、これはここに留まりません。前述した通りイベントの中には状態を変更する情報を含まないけれども、例えばメッセージ(イベントハンドラに応じて電子メールやSMSなどさまざまなものがある)をユーザに通知する必要があるということを示すものもあります。これらすべてについてドメインイベントを使っているために、すべてはイベント格納庫に保存されるので、履歴を保持することができます。

結果整合性("Eventual Consistency")

普通、CQRSの実装を始めた場合には、イベントの格納とレポート用データベースの更新が同一のスレッドで実行されるよう、直接公開を行う仕組みから始めるでしょう。この手法を採用した場合には、結果整合性に関する問題は発生しません。

しかし、システムが大きくなるにつれて、なんらかの性能問題が発生するでしょうし、その時にはバスを実装してイベントの公開とこれらイベントの処理を分けはじめるかもしれません。これが意味するのは、イベント格納庫とリポート用データベースが相互に同期していない可能性があり、その可能性が高いということです。両者が整合するのは結果的なのです。つまり、ユーザが画面上で古いデータを見る可能性があるということです。


これがどの程度クリティカルであるかに応じて、この問題について別の対策を取ることもできます。これについてはサンプルに含まれていませんので、別の機会に紹介したいと思います。

仕様("Specifications")

このアーキテクチャを用いて書くことができる仕様のことを、私は心底気に入っています。やるべきことは、顧客と話し合い、このプロセスがどう機能してほしいと考えているかを聞くことです。こんなシナリオも考えられます。(※訳註2)


ある口座から資金を引き出す

これはどのように行われるでしょうか。まず、預金引出を行うためには、顧客はこの銀行において予め口座を開設し、資金を入れておく必要があります。そして引出が行われた場合には、口座残高は正しい金額に減らされなければなりません。つまり、口座が開設され、かつ、現金が預けられている状態で(given)、現金引出が行われた場合には(when)、現金引出イベントが発生する(then)のです。

namespace Test.Fohjin.DDD.Scenarios.Withdrawing_cash
{
    public class When_withdrawing_cash : CommandTestFixture<WithdrawlCashCommand, WithdrawlCashCommandHandler, ActiveAccount>
    {
        protected override IEnumerable<IDomainEvent> Given()
        {
            yield return PrepareDomainEvent.Set(new AccountOpenedEvent(Guid.NewGuid(), Guid.NewGuid(), "AccountName", "1234567890")).ToVersion(1);
            yield return PrepareDomainEvent.Set(new CashDepositedEvent(20, 20)).ToVersion(1);
        }

        protected override WithdrawlCashCommand When()
        {
            return new WithdrawlCashCommand(Guid.NewGuid(), 5);
        }

        [Then]
        public void Then_a_cash_withdrawn_event_will_be_published()
        {
            PublishedEvents.Last().WillBeOfType<CashWithdrawnEvent>();
        }

        [Then]
        public void Then_the_published_event_will_contain_the_amount_and_new_account_balance()
        {
            PublishedEvents.Last<CashWithdrawnEvent>().Balance.WillBe(15);
            PublishedEvents.Last<CashWithdrawnEvent>().Amount.WillBe(5);
        }
    }
}

これはいいでしょう。ここで口座に十分な資金がないということ以外はまったく同じストーリーが考えられます。この場合には例外をスローしなければなりません。

namespace Test.Fohjin.DDD.Scenarios.Withdrawing_cash
{
    public class When_withdrawling_cash_from_an_account_account_with_to_little_balance : CommandTestFixture<WithdrawlCashCommand, WithdrawlCashCommandHandler, ActiveAccount>
    {
        protected override IEnumerable<IDomainEvent> Given()
        {
            yield return PrepareDomainEvent.Set(new AccountOpenedEvent(Guid.NewGuid(), Guid.NewGuid(), "AccountName", "1234567890")).ToVersion(1);
        }

        protected override WithdrawlCashCommand When()
        {
            return new WithdrawlCashCommand(Guid.NewGuid(), 1);
        }

        [Then]
        public void Then_an_account_balance_to_low_exception_will_be_thrown()
        {
            CaughtException.WillBeOfType<AccountBalanceToLowException>();
        }

        [Then]
        public void Then_the_exception_message_will_be()
        {
            CaughtException.WithMessage(string.Format("The amount {0:C} is larger than your current balance {1:C}", 1, 0));
        }
    }
}

ここで優れているのは、ドメイン全体がブラックボックスと見なされ、使われているのと全く同じ状態に変更し、アプリケーションが実行するであろうコマンドを公開し、その後でドメインが正しいコマンドを公開し、その値が正しいことを確認します。これが意味しているのは、ドメインが自然に至ることのない状態である場合をテストしないということです。これによりテストはより信頼性の高いものになります。


ここであらゆるクラス名を受け取るパーサを想像してみて下さい。各クラス名について現在の状態に至るために発生したイベント出力することになります。続いて、テスト対象であるコマンドを出力します。そして最後に、実際の出力結果をテストするメソッド名を出力します。これはきわめて可読性の高い仕様であり、顧客は少なくとも理解することができます。

他のメリット

この種のアーキテクチャを採用することで得られるメリットとしてもう1つ強調しておきたいものがあります。それは異なるチーム間で作業負荷を分担するのがきわめて簡単であるということです。これはチーム間に時差がある場合に特に言うことができます。ドメインロジックは正しくなければならないものです。これは単価の高い開発者を投入したいと思う場所でしょう。つまり、業務を理解し、正しいコーディングプラクティスを理解した開発者をということです。言っていることは分かりますよね?しかし、参照部分はそれほど重要ではありません。もちろん正しい必要はあるのですが、価値がある場所ではなく、素早く作って1、2年のうちに作り替えることができます。つまり、単価の低い開発者に作ってもらうことができるものだということです。ドメインに関する知識が多く求められることもなく、本当に重要なのは、GUIがどのように機能し、どのようなコマンドが使え、どのようなイベントが要求されるかということだけです。


これについて、私はビジネスにおけるきわめて大きな価値だと思っています。これは見過ごされやすいものでもあります。

最後に

見て頂いた通り、これはきわめてシンプルです。これはまったく異なる考え方ですが、馴染んでしまえば、CRUDよりもふるまいの記述力が高い("behavioral")アプリケーションになることが分かるでしょう。そして顧客も、私たちがこれまで押し付けてきたようなCRUD的考え方ではなく、ビジネスロジックに考えることに立ち戻ることができるのです。最後に、Greg Young氏に感謝したいと思います。氏は多くの情報を提供してくれ、私の下らない質問に耐えてくれました。また、Jonathan Oliver氏とMike Nichols氏にも感謝します。両氏は技術的な面でいくつか改善してくれました。




訳註

1:Event Sourcingについて
集約ルートを永続化する際に「最新の状態」ではなく、「イベントの履歴」を保存する方式は、"Event Sourcing"と呼ばれているパターンです。厳密にはCQRSとEvent Sourcingは異なるパターンですが、Greg Young氏はCQRSとEvent Sourcingとの間に強い相互関係があると考えています。


2:仕様について
given / when / thenという表現から明らかな通り、この「仕様」という表現はふるまい駆動開発(BDD:Behaviour Driven Development)の文脈で理解するべきものです。BDDについてはDan North氏による紹介記事を以前翻訳しました。

Cassandraデータモデル入門 - Arin Sarkissian

この記事は、Arin Sarkissian氏のブログ記事「http://arin.me/blog/wtf-is-a-supercolumn-cassandra-data-model」を氏の許可を得て翻訳したものです。(原文公開日:2009年9月1日)




ここ1、2ヶ月というもの、DiggのエンジニアリングチームはCassandraについて調べ、遊び、最終的にはプロダクションにデプロイするためにかなりの時間を費やしてきました。これは実に楽しいプロジェクトでしたが、楽しくなる前にCassandraのデータモデルについて理解するために相当の時間を費やしたのです。「'super column'って何だよ」というフレーズが何度も口にされました。:)


もしあなたのバックグラウンドがRDBMSならば(ほとんどみんながそうでしょうが)、Cassandraのデータモデルについて学ぶ際に、いくつかのネーミング規約でつまづくことになるでしょう。私やDiggのチームメンバは、「解る」まで2日間に渡って徹底的に話し合ったのでした。最近、開発者メーリングリストで行われた熱い議論も、いくつかの混乱を避けるために、全くあたらしい命名スキームを提案するものでした。議論を通じて私が考え続けていたのは次のようなことです。「おそらくはまともな具体例が手の届く所にあれば、ネーミングのせいでこんなに混乱が起きることはないだろう。」そうしたわけで、私がCassandraのデータモデルを説明するにあたって用いるのは、このスタブです。これは実際にやってみる上での助けになることを意図したもので、詳細には立ち入りません。しかし、いくつかの考え方を明らかにするのには役に立ちます。


ところで、この記事は長いです。PDFバージョンが必要ならば、ここからダウンロードできます(英文)。

構成要素

まずは構成要素を見てから、それらがどのように結びつけられるかを見ていきましょう。

Column

columnは最下位で最も小さいデータの単位です。これは3つ子のタプルで、name、value、timestampを持ちます。


JSON風の記法でcolumnを表現すると次のようになります。

{  // Column
    name: "emailAddress",
    value: "arin@example.com",
    timestamp: 123456789
}

これだけです。単純化するために、タイムスタンプは無視しましょう。nameとvalueのペアだと思って下さい。


ちなみに、namevalueがどちらもバイナリ(実質はbyte[])であり、どんなサイズにもなり得ます。

SuperColumn

SuperColumnは、バイナリのnameと、マップのvalueを持つタプルです。このマップはColumnのnameをキーとして、複数のColumnを含みます。先ほどのJSON風記法を使いましょう。

{   // SuperColumn
    name: "homeAddress",
    // 複数のColumnを含む
    value: {
        // キーはColumnのname
        street: {name: "street", value: "1234 x street", timestamp: 123456789},
        city: {name: "city", value: "san francisco", timestamp: 123456789},
        zip: {name: "zip", value: "94107", timestamp: 123456789},
    }
}
Column 対 SuperColumn

ColumnSuperColumnはどちらも、nameとvalueを持つタプルです。ポイントとなる違いは、通常のColumnvalueが文字列であるのに対し、SuperColumnvalueColumnのマップであるということです。これが主な違いです・・・valueは異なるタイプのデータを保持するのです。もう1つ、より些細な違いをあげれば、SuperColumnはtimestampを持ちません。

本格的に取りかかる前に

先に進む前に、2つのやり方で記法を単純化したいと思います。1) Columnからtimestampを省略する。2) キーと値のペアに見えるように、ColumnSuperColumnのnameを外に出す。こうではなく

{ // super column
    name: "homeAddress",
    // columnのリスト
    value: {
        street: {name: "street", value: "1234 x street", timestamp: 123456789},
        city: {name: "city", value: "san francisco", timestamp: 123456789},
        zip: {name: "zip", value: "94107", timestamp: 123456789},
    }
}

こうなります

homeAddress: {
    street: "1234 x street",
    city: "san francisco",
    zip: "94107",
}

グルーピングする

ColumnSuperColumnをグループ化するのに使われる構造が1つあります。この構造はColumnFamilyと呼ばれるもので、StandardSuperの2種類があります。

ColumnFamily

ColumnFamilyは、複数の行("Row")を持つ構造です。え?ですって?そう、です:)この用語は、RDBMSにおいてテーブルを考えるかのようにして、頭に馴染ませるために使っています。


さて、はクライアント(つまりあなた)によって与えられたキーと、Columnのマップを持ちます。繰り返しますが、マップにおけるキーはColumnのnameであり、valueColumnそれ自体です。

UserProfile = { // ColumnFamily
    phatduckk: {   // ColumnFamily内の行に対するキー
        // 行の中には複数のcolumnがある
        username: "phatduckk",
        email: "phatduckk@example.com",
        phone: "(900) 976-6666"
    }, // 行の終わり
    ieure: {   // ColumnFamily内の別の行に対するキー
        // さっきとは別に複数のColumnがある
        username: "ieure",
        email: "ieure@example.com",
        phone: "(888) 555-1212"
        age: "66",
        gender: "undecided"
    },
}

備忘録: 単純化するために、Columnvalueだけを示していますが、実際にはマップの中のvalueColumn全体です。


これについて、HashMap/dictionary連想配列と考えることもできます。このように考え始めれることができれば、正しい方向に向かっていると言えます。


指摘しておくべきは、このレベルで強要されるスキーマがないということです。は、保持するColumnのリストについて事前に解っていません。上記の例では、"ieure"をキーに持つ行には"age"と"gender"というnameを持つカラムがありましたが、"phatduckk"というキーで特定される行にはそのカラムがありません。これは、100%フレキシブルなものです。1つのが1,989のColumnを持つのに対して、別のが2つしかColumnを持たないということはあり得ます。また、あるが"foo"というColumnを持つのに、他のにはそれがないということもあり得ます。これがCassandraが持つスキーマレスという特性です。

ColumnFamilyにもSuperがある

ColumnFamilyStandard型かSuper型になります。


今見てきたのは、Standard型の例でした。Standardたる所以は、すべてのが含んでいるのが普通の(つまり、Superではない)Columnであるということです。SuperColumnは登場しません。


ColumnFamilySuper型である場合は逆になります。各SuperColumnのマップを含みます。このマップは、各SuperColumnのnameをキーとし、SuperColumn自体を値として持ちます。そして、はっきりさせておきますが、このColumnFamilySuper型であるからと言って、Standard型のColumnFamilyが中に含まれるわけではありません。例を示します。

AddressBook = { // Super型のColumnFamily
    phatduckk: {    // Super ColumnFamily内の行に対するキー
        // ここでキーはアドレスブックのオーナーの名前

        // この行の中にはsuper columnが無制限に入る
        // この行のキーはSuperColumnのnameである
        // これらSuperColumnの1つ1つがアドレスブックのエントリとなる。
        friend1: {street: "8th street", zip: "90210", city: "Beverley Hills", state: "CA"},

        // これがphatduckkのアドレスブックにおけるJohnのエントリである
        John: {street: "Howard street", zip: "94404", city: "FC", state: "CA"},
        Kim: {street: "X street", zip: "87876", city: "Balls", state: "VA"},
        Tod: {street: "Jerry street", zip: "54556", city: "Cartoon", state: "CO"},
        Bob: {street: "Q Blvd", zip: "24252", city: "Nowhere", state: "MN"},
          ...
        // ここにSuperColumn(つまりアドレスブックのエントリ)を無制限に入れることができる。
    }, // 行の終わり
    ieure: {     // これはSuper ColumnFamily内にある別の行に対するキーである
        // ieure用のすべてのアドレスブックエントリ
        joey: {street: "A ave", zip: "55485", city: "Hell", state: "NV"},
        William: {street: "Armpit Dr", zip: "93301", city: "Bakersfield", state: "CA"},
    },
}
Keyspace

Keyspaceは最も外側でデータのグルーピングを行うものです。ColumnFamilyはすべてKeyspaceに含まれます。おそらく、Keyspaceの名前はアプリケーションにちなんで付けられるでしょう。


ここで、1つのKeyspaceは複数のColumnFamilyを持つことができますが、そこに関連がなくても構いません。例えば、MySQLのテーブルと異なり、結合することはできません。同じように、ColumnFamily_1に"phatdukk"というキーを持ったがあるからと言って、ColumnnFamily_2に同じものがあるとは限りません。

Sorting

さて、ここまで様々なデータコンテナがどのようなものであるのかを見てきました。データモデルにおけるもう一つのポイントは、データのソートのされ方です。CassandraをSQLのように検索することはできません。フェッチを行う際にどうやってソートするかを決めることはできないのです(他にも色々違いはありますが)。データはクラスタに投入した瞬間にソートされ、常にソートされた状態になっているのです。これはリード時のパフォーマンスを大幅に向上させますが、その代償としてデータモデルの策定時にアクセスパターンに合ったようなやり方で設計しなければなりません。


Column内において常にColumnのnameによってソートされます。重要なことなので繰り返しますが、Columnは常にnameによってソートされるのです。ソートにあたってのnameの比較は、ColumnFamilyCompareWithオプションによって行われます。選べるものとしては以下のものがあります。BytesType、UTF8Type、LexicalUUIDType、TimeUUIDType、AsciiType、それにLongTypeです。それぞれのオプションは、Columnのnameを異なるデータ型として扱うのであり、それによって少なからぬ柔軟性を得ることができます。例えば、LongTypeを用いると、Columnのnameは64bitのLongとして扱われます。これについて、ソート前後のデータを見ることで、明らかにしていきましょう。

    // これは特定の行においてランダムに並んだすべてのColumnを示したもの。
    // ただし、Cassandraがデータをランダムに格納することは「決して」ありえないので、
    // これは単なる具体例にすぎない。
    // 同様に、valueについても、ソートには一切関係ないので無視できる。
    {name: 123, value: "hello there"},
    {name: 832416, value: "kjjkbcjkcbbd"},
    {name: 3, value: "101010101010"},
    {name: 976, value: "kjjkbcjkcbbd"}

LongTypeを使用していることから、これらのColumnがソートされると以下のようになります。

    &lt;!--
    storage-conf.xmlにおけるColumnFamilyの定義
    --&gt;
    &lt;ColumnFamily CompareWith="LongType" Name="CF_NAME_HERE"/&gt;

    // 各カラム名は64bit long型として扱われる。
    // その結果、上記のColumnはnameの数字的な順序によって並べられる。
    {name: 3, value: "101010101010"},
    {name: 123, value: "hello there"},
    {name: 976, value: "kjjkbcjkcbbd"},
    {name: 832416, value: "kjjkbcjkcbbd"}

ご覧の通り、Columnのnameは64bit Long(つまり、かなり大きな数を格納できる数字)として比較されます。ここで、別のCompareWithオプションを用いれば、結果も異なります。CompareWithUTF8Typeに設定すると、ColumnのnameはUTF8形式にエンコードされた文字列として扱われ、ソート順は以下のようになります。

    &lt;!--
    storage-conf.xmlにおけるColumnFamilyの定義
    --&gt;
    &lt;ColumnFamily CompareWith="UTF8Type" Name="CF_NAME_HERE"/&gt;

    // 各カラムはUTF8の文字列として扱われる
    {name: 123, value: "hello there"},
    {name: 3, value: "101010101010"},
    {name: 832416, value: "kjjkbcjkcbbd"},
    {name: 976, value: "kjjkbcjkcbbd"}

このように、結果は全く異なります!


ソートのこの原則は、SuperColumnにも同じように当てはまります。しかしこれについては、別の次元が存在します。つまり、SuperColumnにおいてどのようにソートされるかを決めるだけでなく、各SuperColumnの中でColumnがどのようにソートされるかも決定しなければならないのです。各SuperColumn内のColumnのソートは、CompareSubcolumnsWithの値によって決定されます。例を示します。

    // 2つのSuperColumnを持つ行の見え方を示す。    
    // 現在はランダムに並んでいる。

    { // 行における最初のSuperColumn
        name: "workAddress",
        // そこに含まれるColumn
        value: {
            street: {name: "street", value: "1234 x street"},
            city: {name: "city", value: "san francisco"},
            zip: {name: "zip", value: "94107"}
        }
    },
    { // 同じ行のもう一つのSuperColumn
        name: "homeAddress",
        // そこに含まれるColumn
        value: {
            street: {name: "street", value: "1234 x street"},
            city: {name: "city", value: "san francisco"},
            zip: {name: "zip", value: "94107"}
        }
    }

ここで、CompareSubcolumnsWithCompareWithの両者をUTF8Typeに設定すると、以下の結果になります。

    // ソート後

    {
        // UTF8の文字列として扱われるので、こちらが先になる
        { // 同じ行のもう一つのSuperColumn

            // "homeAddress"は"workAddress"よりも前なので、この行が先に来る
            name: "homeAddress",

            // SuperColumn内のcolumnもnameによってソートされる
            value: {
                // これらもColumnのnameによってソートされる
                city: {name: "city", value: "san francisco"},
                street: {name: "street", value: "1234 x street"},
                zip: {name: "zip", value: "94107"}
            }
        },
        {
            name: "workAddress",
            value: {
                // SuperColumn内のcolumnもnameによってソートされる
                city: {name: "city", value: "san francisco"},
                street: {name: "street", value: "1234 x street"},
                zip: {name: "zip", value: "94107"}
            }        
        }
    }

最後の例では、 CompareSubcolumnsWithCompareWithの両方がUTF8Typeに設定されていますが、必ずしもそうする必要はありません。CompareSubcolumnsWithCompareWithは必要に応じて別々のものを組み合わせることができます。


ソートに関して最後に追記すると、ソートを実行するためのカスタムクラスを作ることもできます。ソートの機構はプラガブルになっており、CompareSubcolumnsWithCompareWithの値には、org.apache.cassandra.db.marshal.ITypeを実装したクラスのフルパス名を設定することができます(つまり、カスタム比較子を作ることができるのです)。

スキーマの具体例

さて、パズルのピースが揃ったところで、それを組み合わせて、シンプルなブログアプリケーションのモデルを作ってみましょう。以下の仕様でシンプルなアプリをモデリングします。

  • 単一のブログをサポートする。
  • 複数の著者が存在できる。
  • エントリには、タイトル、本文、スラグ(エントリのパス)、公開日付が含まれる。
  • エントリには複数のタグがつけられる。
  • コメントを残すことはできるが、自分を登録することはできず、毎回プロフィールを打ち込む必要がある(シンプルにするため)。
  • コメントには次のものが含まれる。テキスト、時間、コメントを書いた人の名前とメールアドレス。
  • すべてのポストは時系列の逆順で新しいものが先頭にくるように表示しなければならない。
  • 与えられたタグを含むすべてのポストを時系列の逆順で表示しなければならない。

以下の節では、このアプリのKeyspaceにおいて定義されるColumnFamilyについて説明します。まず、xmlの定義を示し、特定のソートオプションを選択したことの理由について説明すると同時に、JSON風の記法を用いてColumnFamilyのデータを表示します。

Authors ColumnFamily

著者ColumnFamilyモデリングは、実に基本的で、特別なことは行いません。各著者に対して対応するが与えられ、著者のフルネームがキーとなります。の中で、各Column著者の「プロフィール」の各属性を表します。


これは、各を使ってオブジェクトを表現している例になります。この場合は、著者オブジェクトです。このアプローチにおいて、各Columnは属性として扱われます。実にシンプルですが、1点指摘しておきます。特定のにおいてどのColumnがなければならないかということについての「定義」はありませんので、ある種のスキーマレスな設計を行うことになります。


このColumnFamilyにおけるに対するアクセスはキーのルックアップによって行われ、キーに合致したものについてはすべてのColumnが取得されます(上記の例でいうと、存在しない'foo'というキーでアクセスした場合に、内の3つのcolumnがフェッチされることはありません)。ここから、Columnがどのようにソートされているかを気にする必要はないので、ソートオプションとしてはBytesTypeを使うことにします。Columnのnameについてバリデーションを行う必要がないためです。

&lt;!--
    ColumnFamily: Authors
    著者の情報はすべてここに保持する。    
    行のキー =&gt; 著者の氏名(氏名は一意でなければならない)
    ColumnName: エントリの属性(titlebody、など)
    ColumnValue: 属性に紐づくvalue

    アクセス: 著者を氏名で取得する(つまり特定の行からすべてのcolumnを取得する)

    Authors : { // ColumnFamily
        Arin Sarkissian : { // 行のキー
            // 「プロフィール」の属性を示すcolumn
            numPosts: 11,
            twitter: phatduckk,
            email: arin@example.com,
            bio: "bla bla bla"
        },
        // 別の著者
        Author 2 {
              ...
        }
    }
--&gt;
&lt;ColumnFamily CompareWith="BytesType" Name="Authors"/&gt;
BlogEntries ColumnFamily

ここでも、ColumnFamilyに対してはシンプルなキー/バリュー検索が行われます。各に対して1エントリが保持されます。このにおいて、Columnはエントリの各属性、つまりタイトル、本文などとして扱われます(前の例と同様です)。若干の最適化が施されており、タグをColumnに非正規化するにあたってカンマ区切りの文字列としています。画面上はColumnの値を分割してタグのリストを取得します。


に対するキーはエントリのスラグになります。つまり、単一のエントリを取得したいと思った時には、キー(スラグ)で検索すれば良いことになります。

&lt;!--
    ColumnFamily: ブログエントリ
    ここにすべてのブログエントリが入る

    行のキー =&gt; ポストのスラグ(uri内のseo対策となる部分)
    ColumnName: エントリの属性(titlebody、など)
    ColumnValue: 属性に紐づくvalue

    アクセス: スラグによってエントリをすべて取得する(該当する行内のすべてのColumnを常にフェッチする)

    参考: タグは非正規化され、カンマ区切りのリストになっている    
    ここで用いる記法と衝突するのを避けるためJSONは使わないが、
    作成するアプリケーションが扱い方を理解している限り、
    どのような記法も使うことができる

    BlogEntries : { // ColumnFamily
        i-got-a-new-guitar : { // 行のキー - エントリの一意の「スラグ」            
            title: これは僕の新品の素晴らしいギターについてのエントリです
            body: かっこいいエントリかくかくしかじか
            author: Arin Sarkissian  // Authors CFのキー
            tags: life,guitar,music  // カンマ区切りのタグリスト(基本的な非正規化)
            pubDate: 1250558004      // 公開日のunixtime
            slug: i-got-a-new-guitar
        },
        // その他のエントリ
        another-cool-guitar : {
              ...
            tags: guitar,
            slug: another-cool-guitar
        },
        scream-is-the-best-movie-ever : {
              ...
            tags: movie,horror,
            slug: scream-is-the-best-movie-ever
        }
    }
--&gt;
&lt;ColumnFamily CompareWith="BytesType" Name="BlogEntries"/&gt;
TaggedPosts ColumnFamily

さて、ここから少し面白くなってきます。このColumnFamilyには、ちょっとした重労働があります。これには、タグとエントリの関連を保持するという役割がありますが、これは関連を保持するだけでなく、これによって特定のタグに紐づくすべてのBlogEntryをあらかじめソートされた順序でフェッチすることができるようになります(前述したソートの話を思い出して下さい)。


ここで挙げておくべき設計上のポイントとしては、アプリケーションの論理的なタグを作り、すべてのBlogEntryに"__notag__"タグを設定するというものがあります(これは今作ったタグです)。すべてのBlogEntryに"__notag__"タグを設定することで、このColumnFamilyを使ってすべてのBlogEntryのリストを事前にソートされた順序で保存することができるようになるのです。ある意味ごまかしているようですが、それによって一つのColumnFamilyで、「最近のポストをすべて表示」と「'foo'タグがついた最近のポストをすべて表示」の両方ができるようになります


このデータモデルに従うと、エントリに3つタグがあった場合、対応するColumnが4つのの中にあることになります。つまり、それぞれのタグに対して1つずつ、"__notag__"タグに対して1つです。


エントリのリストを時系列順に表示したいので、各ColumnのnameをtimeUUIDとし、ColumnFamilyCompareWithTimeUUIDTypeに設定する必要があります。これにより、Columnは時間によってソートされ、「時系列順」という要求を満たします:)したがって、「'foo'タグがついた最近の10エントリ」といった処理が、極めて効率的になるのです。


最近の10エントリを表示したい(例えばフロントページに)と思った時には以下の処理を行います。

  1. 内にある最新の10Columnを、"__notag__"(「全ポスト」タグ)をキーとして取得する。
  2. 取得したColumnをループする。
  3. ループに際して、各Columnの値がBlogEntriesColumnFamily内のに対するキーであることが分かっている。
  4. そこで、このキーをBlogEntriesColumnFamilyから、エントリに対応するを取得するのに使う。これによって、このエントリのすべてのデータが取得できる。
  5. 取得したBlogEntriesが持つColumnの1つが、"author"と名付けられており、そのvalueAuthorsColumnFamilyのキーとなっている。これは著者のプロフィール情報を取得するのに必要である。
  6. ここで、エントリの情報と著者の情報を手にしたことになる。
  7. 次に、"tags"Columnvalueを分割してタグのリストを取得する。
  8. これでこのポストを表示するのに必要な情報がすべて取得できたことになる(まだコメントがないが、permalinkページではないのでよしとする)。

上記の処理はどんなタグに対しても行うことができます。したがって、「すべてのエントリ」にも「'foo'タグのついたエントリ」に対しても有効なのです。悪くないでしょう。

&lt;!--
    ColumnFamily: TaggedPosts
    あるタグに対してどのブログエントリが紐づけられているかを決定する2次インデックス

    行のキー =&gt; tag
    Column Names: TimeUUIDType
    Column Value: BlogEntries CclumnFamilyにおける行のキー

    アクセス: 'foo'タグのついたエントリのスライスを取得する

    このColumnFamilyは、あるタグのページに対してどのブログエントリを表示するかを決めるのに使用する     
    多少小細工をして、__notag__という文字列を使って「タグで制限されていない」ということを意味している
    各エントリはここにあるcolumnを取得する
    つまり、各ポストに対してタグの数+1のcolumnができることになる

    TaggedPosts : { // ColumnFamily
        // "guitar"タグがついたブログエントリ
        guitar : {  // タグのnameが行のキーになる
            // columnのnameはTimeUUIDTypeであり、valueがBlogEntriesの行に対するキーである
            timeuuid_1 : i-got-a-new-guitar,
            timeuuid_2 : another-cool-guitar,
        },
        // ここに全ブログエントリが入る
        __notag__ : {
            timeuuid_1b : i-got-a-new-guitar,

            // これはguitar行にも存在する
            timeuuid_2b : another-cool-guitar,

            // これはmovie行にも存在する
            timeuuid_2b : scream-is-the-best-movie-ever,
        },
        // "movie"タグがついたブログエントリ
        movie: {
            timeuuid_1c: scream-is-the-best-movie-ever
        }
    }
--&gt;
&lt;ColumnFamily CompareWith="TimeUUIDType" Name="TaggedPosts"/&gt;
Comments ColumnFamily

最後に、コメントをどのようにモデリングするのか見ていきましょう。ここではある種のSuperColumnが登場します。


各エントリに対して、1つのが対応します。に対するキーはエントリのスラグになります。各内には、各コメントに対応するSuperColumnが存在します。SuperColumnのnameはUUIDなので、TimeUUIDTypeを採用することになります。これにより、エントリに対するコメントは時系列順にソートされることになります。各SuperColumn内のColumnはコメントにある様々な属性です(コメントを書いた人の名前、コメントの時刻など)。


これも非常にシンプルで、特別なことはなにもありません。

&lt;!--
    ColumnFamily: Comments
    すべてのコメントがここに入る

    行のキー =&gt; BlogEntryの行のキー
    SuperColumn name: TimeUUIDType

    アクセス: エントリに対するすべてのコメントを取得する

    Comments : {
        // scream-is-the-best-movie-everに対するコメント
        scream-is-the-best-movie-ever : { // 行のキー = BlogEntryの行のキー
            // 最も古いコメントが先に来る
            timeuuid_1 : { // SuperColumn Name
                // SuperColumn内のすべてのColumnはコメントの属性である
                commenter: Joe Blow,
                email: joeb@example.com,
                comment: 何を言っているんだ、ゴッドファーザーが一番に決まっている
                commentTime: 1250438004
            },

            scream-is-the-best-movie-everに対する他のコメント

            // 最新のコメントが最後
            timeuuid_2 : {
                commenter: Some Dude,
                email: sd@example.com,
                comment: 落ち着けJoe Blow、これはyoutubeじゃないんだ
                commentTime: 1250557004
            },
        },

        // i-got-a-new-guitarへのコメント
        i-got-a-new-guitar : {
            timeuuid_1 : { // SuperColumn Name
                // SuperColumn内のすべてのColumnはコメントの属性である
                commenter: Johnny Guitar,
                email: guitardude@example.com,
                comment: nice axe dawg...
                commentTime: 1250438004
            },
        }

        ..
        // 他のエントリに対するSuperColumn
    }
--&gt;
&lt;ColumnFamily CompareWith="TimeUUIDType" ColumnType="Super"
&nbsp;&nbsp;&nbsp;&nbsp;CompareSubcolumnsWith="BytesType" Name="Comments"/&gt;

やったぞ!

これでおしまいです。この小さいブログアプリはモデリングされ、準備ができました。要約するにはちょっとした量でしたが、最終的にstorage-conf.xmlに記述するXMLはとても小さいものになります。

    <Keyspace Name="BloggyAppy">
        <!-- 他のkeyspaceコンフィグ -->

        <!-- ColumnFamily定義 -->
        <ColumnFamily CompareWith="BytesType" Name="Authors"/>
        <ColumnFamily CompareWith="BytesType" Name="BlogEntries"/>
        <ColumnFamily CompareWith="TimeUUIDType" Name="TaggedPosts"/>
        <ColumnFamily CompareWith="TimeUUIDType" Name="Comments"
            CompareSubcolumnsWith="BytesType" ColumnType="Super"/>
    </Keyspace>

残るは、Cassandraとデータのやり取りをするのにどうすれば良いかを明らかにするだけです;)。これは、Thriftのインタフェースを通じて行われます。様々なエンドポイントで何ができるのかを説明するにあたってはAPIのwikiページが良い仕事をしているので、ここでは詳細に立ち入りません。しかし、一般的には、cassandra.thriftファイルをコンパイルし、さまざまなエンドポイントにアクセスするのに、生成されたコードを使うことになるでしょう。そうでなければ、RubyクライアントPythonクライアントが使えるでしょう。


さて、この記事が役に立ち、読者がSuperColumnとはいったい何なのかを理解して、素晴らしいアプリを作り始めてもらえたら幸いです。

DCIによるWebアプリケーション - 3:Cassandra

Cassandraを使用して割り勘アプリケーションの永続化レイヤを実装する。

導入

割り勘アプリケーション実装の第2部では、WebフレームワークとしてWicketを使用し、傾斜つきの割り勘計算がとりあえず行える所までを実装しました。しかし、データはメモリ上で保持しているだけで、永続化レイヤの実装は課題となっていました。そこで今回はCassandraを使用して永続化レイヤを実装していきます。


バックナンバー

永続化レイヤの実装

前回使用したアーキテクチャの概要図を再掲します。

ここまでDIコンテナの使用は意図的に避けてきましたが、外部リソースへのアクセスが必要になった所で使うことにします(Guice-2.0)。DIコンテナを使う場合、オブジェクトのウィービングをどのレイヤから行うかは設計上の一つの判断だと思いますが、今回はドメインレイヤに影響を与える事を避けるため、リポジトリクラスの委譲クラスを作成し、それをDIすることにしました。


例としてPartyRepositoryの実装を示します。

object PartyRepository {

    val repository:PartyRepositoryImpl = Injector.getInstance(classOf[PartyRepositoryImpl])
	
    def nextId:String = repository.nextId
	
    def add(party:Party) = repository.add(party)

    ...

InjectorはGuiceAPIをラップしたヘルパクラスです。

object Injector {
    ...
    def getInstance[T](clazz:Class[T]):T = {
        Guice.createInjector(_config).getInstance(clazz)
    }
}

リポジトリクラスを境界としてテクニカルな領域を切り離すことにより、永続化レイヤの実装の変化はRepositoryを使用していたクラスに対して影響を与えません。したがって、これ以降の話は、DCIアーキテクチャに関係なく、純粋なCassandraの実装の話と考えることができます。今回、永続化レイヤの実装がドメインモデルに与えた影響は、UUIDを使用する事に伴ってIDがIntからString型に変化したことだけです。

Cassandra

ご存知の通り、Apache Cassandraは「第2世代」と言われる分散データベースです。セットアップ自体は容易で、その意味での敷居は低いですが、実際にアプリを作ろうと思うとデータモデルとAPIが壁になるでしょう。


誤解を恐れずに要約するならば、Cassandraのデータモデルを理解するポイントは以下の2点に集約できると思います。

  • キー」と「カラム」がすべての中心。
  • カラム=縦」というRDBの発想を捨てる。

その上で、SuperColumnとSuperColumnFamilyという概念が正確にイメージできるようになる必要があります。以下、PartyエンティティとPartyリポジトリ実装を例に見て行きます。

データモデル

Partyエンティティのカラムファミリーを示します。

<ColumnFamily CompareWith="UTF8Type" Name="Party" />
<ColumnFamily CompareWith="UTF8Type" Name="Participant" CompareSubcolumnsWith="UTF8Type" ColumnType="Super" />
<ColumnFamily CompareWith="UTF8Type" Name="Allot" CompareSubcolumnsWith="UTF8Type" ColumnType="Super" />

Partyが通常のColumnFamilyであるのに対して、Participant/AllotはSuperColumnFamilyであることが1つのポイントです。続いてデータオブジェクトとスキーマを示します(スキーマの記法は独自のものです。感覚で理解して下さい)。

class Party(val id:String) {
    var name:String = null
    var schedule:Date = null
    var location:String = null
    var sum:Int = 0
    var participants = Map[String, Participant]() // userName, Participant
    var allots = Map[String, Allot]() // userName, Allot
Party {
    partyId:[(column:{name:partyName}), (column:{name:schedule}), (column:{name:location}), (column:{name:sum})]
    partyId:[...]
    ...
}

PartyカラムファミリーはUUIDであるpartyIdに対して複数のカラムが紐づいています。それに対して、Partyと1対多の関係となるParticipantのスキーマは以下のようになっています。

Participant {
    partyId:[ 
        super_column:{ name:participantId, columns:[ (column:{name:userName}), (column:{name:roleName}) ] },
        super_column:{ name:participantId, columns:[ (column:{name:userName}), (column:{name:roleName}) ] },
        ...
    ]
    partyId:[...]
    ...
}

ここで、SuperColumnのカラム名がキーとして扱われることが1つのポイントです。

データの操作

更新系
APIとしてはキーとカラムをピンポイント指定するinsertおよびremoveと、Mutationのマップを用いてデータモデルを組み上げてから一気に更新するbatch_mutateがあります。ただ、実際に使うのはほぼbatch_mutateでしょう。この操作では、キーを指定して、複数のカラムファミリーを一度に更新できます。


検索系
APIはいくつかありますが、基本的な概念は以下の通りです。

  • get_XXX → キーを1つ指定
  • multiget_XXX → キーを複数指定
  • XXX_slice → カラムを複数指定
  • get_range_slices → キーを範囲指定(sliceがついているので、カラムも複数指定)

おそらく、ここで最も重要かつ混乱の原因になるのはsliceの挙動です。以下、get_sliceを例に解説します。


get_sliceのシグニチャは以下の通りです。

list<ColumnOrSuperColumn> get_slice(string keyspace, string key, ColumnParent column_parent, SlicePredicate predicate, ConsistencyLevel consistency_level)

カラムファミリーであるPartyに対してpartyIdを指定したget_sliceを実行した場合、取得されるのはpartyName、scheduleなどのカラムのリストです()。これに対し、スーパーカラムファミリーであるParticipantに対してpartyIdを指定してget_sliceを実行した際に取得されるのは、super_columnのリストです()。このように「キーとカラム」という概念が中心におかれることで、「縦と横」に対する感覚がだいぶ変わっています。この感覚が理解できれば、一通りデータの操作ができるようになるのではないかと個人的には考えています。あとはWikiのAPIを見ながらコツコツ実装という感じでしょうか。書かなければいけないコードの量はかなり多いので、本格開発する時にはなんらかのミドルは入れる必要があるかもしれません。


コードは長くなるので省略しましたが、PartyRepositoryImplの実装に興味のある方はこちらをご参照下さい。また、今回使用したコードサンプルはこちらにタグを切ってあります。また、ローカルにて試される際には環境構築メモをご参照下さい。

まとめ

3回に渡って実装を膨らませてきた割り勘アプリケーションですが、今回で一通り登場人物が出そろいました。機能面での不足(編集・削除ができない)や入力チェック・コネクションプールの未実装など、アプリケーションとして見ればまだまだ課題は残っていますが、DCIアーキテクチャのプロトタイプとしてはこれで完了とします。


要素技術としてはScala-Wicket-Guice-Cassandraを用いましたが、DCIアーキテクチャにおいてこれらの要素技術はそれほど重要ではありません。強いて言えば、DCIの基本思想である「データに対するロールのミックスイン」を実現するために、言語ないしミドルウェアに制約があるという程度でしょうか(今回はScalaのトレイトと抽象メンバを利用して実現しています)。むしろ重要なのは、ビジネスロジックを実装したアーキテクチャであるCQRS風DCIが、これらのインフラから切り離された場所で実装されているということです。実際にプレゼンテーションレイヤとはContext(場合によってはAction)で、永続化レイヤとはRepositoryで切り離されているため、Webフレームワークおよびデータベースは任意の実装に差し替える事ができます。


DDDの冒頭でEric Evans氏は次のように言っています。

Yet the most significant complexity of many application is not technical. It is in the domain itself, the activity or business of the user...
The Premise of this book is twofold:
1. For most software projects, the primary focus should be on the domain and domain logic.
2. Complex domain design should be based on a model


しかし、多くのアプリケーションにおいて最も重要な複雑さは技術的なものではありません。これはドメインそれ自体、つまりユーザの活動やビジネスにあるのです。(中略)
この本の大前提は以下の2つです。
1. ほとんどのソフトウェアプロジェクトにおいて、焦点はまず第一にドメインドメインロジックに絞られるべきです。
2. 複雑なドメインの設計はモデルに基づくべきです。

DCIアーキテクチャの構想においても、「オブジェクトはまず人々とそのメンタルモデルに関するものであり、ポリモルフィズムや結合、凝集に関するものではない」とある通り、主題はユーザのメンタルモデルを捉えることにあります。


言葉は思考を表現するものあると同時に、言葉(概念)は思考を形作るものでもあります。ドメインモデルについて真剣に考え、新しい概念を見つけること、これは思考を変えるものであり、それによって見えていなかったものが見えてくることが確かにあります。DCIアーキテクチャが提供するロールやコンテキストといった概念は、そうした試みにおける「思考のフレームワーク」としておおいに役立つものなのではないでしょうか。

実例で学ぶCassandra - Eric Evans

この記事はEric Evans氏の記事「http://www.rackspacecloud.com/blog/2010/05/12/cassandra-by-example/」を、氏の許可を得て翻訳したものです。(原文公開日:2010年5月12日)





最近、Cassandraは注目を集めており、今まで以上に多くの人が組織で使おうと評価しています。こういった人々がCassandraについて詳しく知ろうとするにつれ、私たちのドキュメントが不足していることが明らかになってきました。その中で最たるものは、既存のリレーショナルデータベースのバックグラウンドを持つ人に対するデータモデルの説明です。


問題はCassandraのデータモデルが、伝統的なデータベースのデータモデルと比べて、混乱を引き起こしかねないほど異なっており、それを正そうとして行われた様々な説明が同じく誤解を生み出しているということです。


モデルについてマップのマップと説明されることがあります。これはスーパーカラムに対してはマップのマップのマップとなります。このような説明は、実例を示すためにJSON風の記法を用いて視覚的に行われることもあります。また、カラムファミリーがスーパーステーブルに喩えられることがあれば、カラムオブジェクトのコレクションを保持するコンテナに喩えられることもあります。時には、カラムが3タプルとされます。私から見るとこれらの説明はいずれも十分ではありません。


問題は、新しいものについて比喩を使わずに説明するのが難しいということです。しかし、比較に説得力がなければ混乱するだけです。Cassandraのデータモデルについて説明するための素晴らしい方法を誰か考えだしてくれないかと思っていますが、差し当たりはその価値に見合うような具体例を見つけることにします。

Twitter

TwitterはCassandraの実際の利用例であると同時に、よく知られたサービスで、容易に概念化できることから議論の題材としても優れています。私たちは例えば次のようなことを知っています。ほとんどのサイトと同じようにユーザ情報(名前、パスワード、e-mailアドレスなど)が全員分あり、それらのエントリはフレンドとフォロワを紐づけるために相互にリンクされています。さらに、ツイートを保存することができなければTwitterとは呼べません。ツイートは140文字のテキストに加えて、タイムスタンプやURLに含まれるユニークなIDのようなメタデータに関連づけられています。


これをリレーショナルデータベース上でモデリングする場合、アプローチは比較的シンプルなものになります。まず、ユーザを保存するテーブルが必要です。

CREATE TABLE user (
    id INTEGER PRIMARY KEY,
    username VARCHAR (64),
    password VARCHAR(64) 
);

さらに、フォローしている人とフォローされている人を返すための1対多を実現するテーブルが必要です。

CREATE TABLE followers (
        user INTEGER REFERENCES user(id),
        follower INTEGER REFERENCES user(id)
);
 
CREATE TABLE following (
        user INTEGER REFERENCES user(id),
        followed INTEGER REFERENCES user(id)
);

もちろん、ツイート自体を保存するテーブルも必要でしょう。

CREATE TABLE tweets (
        id INTEGER,
        user INTEGER REFERENCES user(id),
        body VARCHAR(140),
        timestamp TIMESTAMP
);

デモのため、物事を過度に単純化していますが、このようなちょっとしたモデルであっても、当然のこととされているものが多くあります。例えば、データ正規化のため、実践的には外部キー制約が必要になりますし、複数のテーブルからデータを取得するのに結合を行う必要があるので、それを効率に行うためには適切な属性に任意のインデックスを張る必要があります。


これらに対して、分散システムを正しく稼働させるということは大きな変化であり、トレードオフなしには実現できません。このことはCassandraにおいても該当し、そのため上記のデータモデルは機能しません。まず、参照の整合性は存在しません。また2次インデックスがサポートされないので結合を効率的に行うことができず、そのため正規化を崩す必要があります。別の言い方をすれば、実行するクエリと期待する結果の観点から考える必要があります。おそらくはそれがモデルがどう見えるかということを示すものだからです。

Twissandra

それでは、前述したモデルはCassandraにおいてどう表現されるでしょうか?幸いにも私たちはTwissandraを参照することができます。これは機能を必要最小限に抑えたTwitterのクローンであり、Eric Florenzano氏によりサンプルとして書かれたものです。TwitterとTwissandraを例に用いつつ、Cassandraにおけるデータモデリングについて検討していきましょう。

スキーマ

Cassandraはスキーマレスのデータストアであると考えられています。しかしアプリケーションに応じてある程度の定義を行う必要はあります。Twissandraには、動作に必要なCassandraの定義が備わっていますが、ここで一旦立ち止まり、データモデルに関連した特別な側面を検討する価値はあるでしょう。

キースペース

キースペースとは、Cassandraにおける最上位のネームスペースであり、基本的には各アプリケーションにおいて1つだけ存在します。将来的には、RDBMSにおいてデータベースを作るのと同じように動的に作られる予定ですが、0.6以前では、メインのコンフィグレーションファイルなどで定義されています。

<Keyspaces>
    <Keyspace Name="Twissandra">
    ...
    </Keyspace>
</Keyspaces>
カラムファミリー

各キースペースにおいて、1つ以上のカラムファミリーが存在します。カラムファミリーとは類似のレコードを関連させるために用いられる名前空間です。Cassandraは書き込みの際、レコードレベルのアトミック性をカラムファミリー内において保証し、カラムファミリーに対するクエリは効率的になります。データモデルを設計する際にこの性質を覚えておく事は重要です。その理由は以降の議論で明らかになります。


キースペースと同様、カラムファミリー自体もメインコンフィグにおいて定義されます。将来的には、RDBMSにおいてテーブルを作成するのと似たようなやり方で、オンザフライに生成する事ができるようになります。

<Keyspaces>
    <Keyspace Name="Twissandra"/>
        <ColumnFamily CompareWith="UTF8Type"  Name="User"/>
        <ColumnFamily CompareWith="BytesType" Name="Username"/>
        <ColumnFamily CompareWith="BytesType" Name="Friends"/>
        <ColumnFamily CompareWith="BytesType" Name="Followers"/>
        <ColumnFamily CompareWith="UTF8Type"  Name="Tweet"/>
        <ColumnFamily CompareWith="LongType"  Name="Userline"/>
        <ColumnFamily CompareWith="LongType"  Name="Timeline"/>
    </Keyspace>
</Keyspaces>

上に抜粋したコンフィグでは、カラムファミリーの名前に加えて比較子("comparator")も定義しています。これは伝統的なデータベースとの重要な違いを示すものです。つまり、レコードがソートされる順序は設計レベルの意思決定であり、後で容易に返られるものではないということです。

カラムファミリーとは何か?

これら7つのTwissandraカラムが何のためにあるかは、直感的にすぐ理解できるものではありません。したがって、それぞれについてさらに詳細に見て行きましょう。

  • User

これはユーザが保存される場所であり、上記のSQLスキーマにおけるユーザテーブルに該当します。このカラムファミリーにおいて保存されるレコードはUUIDに紐づけられ、ユーザ名とパスワードというカラムを保持します。

  • Username

上記のユーザカラムファミリーを検索するには、ユーザのキーを知っている必要があります。しかし、ユーザ名しか分からなかった場合、どうすればこのようなUUIDベースのキーを見つける事ができるでしょうか。リレーショナルデータベースと上記のSQLスキーマを用いた場合であれば、ユーザテーブルに対し、ユーザ名をマッチさせるような(WHERE username = ‘jericevans’)SELECTを実行するでしょう。しかし、これは2つの理由からCassandraで機能しません。


まず、リレーショナルデータベースはこういったSELECTを実行する際に、テーブルをシーケンシャルにスキャンします。しかし、Cassandraではレコードがキーに基づいてクラスターの間に分散されているので、同じ事をやろうとすると1つ以上のノードとやり取りすることになります(おそらくそのノードの数は多いでしょう)。しかし、すべてのデータが単一のマシン上にあったとしても、このような操作がリレーショナルデータベースで非効率になることもあり、その場合にはユーザ名属性にインデックスを張る必要が生じます。前述した通り、Cassandraは今のところ、このような2次インデックスをサポートしません。


これに対する解答は、読み取り可能なユーザ名からUUIDに紐づくキーへの転置インデックスを生成することになります。そしてこれこそがカラムファミリーの目的なのです。

  • Friends
  • Followers

FriendsとFollowersカラムファミリーは、ユーザXがフォローしているのは誰か?誰がユーザXをフォローしているか?という問いにそれぞれ答えます。それぞれは一意のユーザIDに紐づいており、対応する関連を追跡するためのカラム群と生成された時刻を保持します。

これは、ツイート自体が保存される場所です。このカラムファミリーが保持しているレコードは、一意のキー(UUID)、ユーザIDに対応するカラム、本文、そしてツイートが追加された時間です。

  • Userline

これは、保存された各ユーザに対応するタイムラインです。このレコードは、ユーザIDキーと、Tweetカラムファミリーにおける一意のツイートIDとタイムスタンプを紐づけるカラムから構成されます。

  • Timeline

このカラムファミリーはUserlineに似ていますが、各ユーザのためにフレンドのツイートのマテリアライズドビューを保持する点が異なります。


上記のカラムファミリーを踏まえて、いくつか一般的な操作を行い、これらがどのように適用されるのかを見て行きましょう。

すべてを結びつける

ユーザを新規追加する

まず、新しいユーザにはアカウントにサインアップする方法が必要です。その際、ユーザはCassandraデータベースに追加されます。Twissandraにおいて、この処理は以下のようになります。

username = 'jericevans'
password = '**********'
useruuid = str(uuid())
 
columns = {'id': useruuid, 'username': username, 'password': password}
 
USER.insert(useruuid, columns)
USERNAME.insert(username, {'id': useruuid})

TwissandraはPythonで書かれており、クライアントアクセスにはPycassaを使用しています。上に示した大文字のUSERとUSERNAMEがpycassaです。ColumnFamilyのインスタンスは、それぞれ"User"と"Username"の初期化の際に別の場所で生成されています。


ここで追記しておくと、上記のコードやこの後に出てくるサンプルは、Twissandraのものをそのまま切り取ったものではありません。より簡潔にになるように、あるいは自分の気に入るように変更を加えています。例えば、上記のコードでは、ユーザ名やパスワードに変数を割り当てる意味はありません。Webアプリケーションにおいて、これらはサインアップページのフォームエレメントから取得されます。


サンプルに戻ると、ここでは2つの異なるCassandraの書き込み(insert())処理が登場します。1つはUserカラムファミリーに新しいレコードを追加する処理であり、もう1つは人間に読む事ができるユーザ名をUUIDキーにマッピングさせる転置インデックスの更新処理です。どちらのケースもinsert()の引数は、後でレコードを検索するのに利用するキーと、カラム名と値を保持するマップです。

フレンドをフォローする
frienduuid = 'a4a70900-24e1-11df-8924-001ff3591711'
 
FRIENDS.insert(useruuid, {frienduuid: time.time()})
FOLLOWERS.insert(frienduuid, {useruuid: time.time()})

ここでも別々のInsert()処理を実行しています。今回の場合は誰かをフレンドのリストに追加し、その関係を逆から追跡するために新しいフォロワーを対象となるユーザに追加しています。

ツイートする
tweetuuid = str(uuid())
body = '@ericflo thanks for Twissandra, it helps!'
timestamp = long(time.time() * 1e6)
 
columns = {'id': tweetuuid, 'user_id': useruuid, 'body': body, '_ts': timestamp}
TWEET.insert(tweetuuid, columns)
 
columns = {struct.pack('&amp;gt;d'), timestamp: tweetuuid}
USERLINE.insert(useruuid, columns)
 
TIMELINE.insert(useruuid, columns)
for otheruuid in FOLLOWERS.get(useruuid, 5000):
    TIMELINE.insert(otheruuid, columns)

新しいツイートを保存するには、新しく生成されたUUIDをキーとして利用し、Tweetカラムファミリーに新しいレコードを生成します。カラムとしては、ツイートした人のユーザIDと生成された時刻、そしてもちろんツイート自体のテキストがあります。


加えて、このユーザのUserlineはツイートの時刻を一意のIDに紐づけるために更新されます。もし、これがユーザの初めてのツイートであれば、insert()の結果新しいレコードが追加され、その後に続くインサートによってこのレコードに新しいカラムが追加されます。


最後に、このユーザとフォロワーのため、Timelineが時刻とツイートIDを紐づけるカラムと共に更新されます。


ここで使用されているタイムスタンプがlong(64 bit)である事は注目に値します。そしてこれがカラム名として与えられた場合には、ネットワークバイトオーダーにおいてバイナリ値としてパッキングされます。この理由から、UserlineとTimelineカラムファミリーはLongType比較子を使用しているのであり、このことによって数述語("numeric predicate")を用いてカラムの範囲検索を行う事ができ、その結果も数字の順序に従ってソートされるのです。

ユーザのツイートを取得する。
timeline = USERLINE.get(useruuid, column_reversed=True)
tweets = TWEET.multiget(timeline.values())

ここで、ユーザからツイートを検索しています。まずUserlineからIDのリストを取得し、その後でmultiget()を用いてTweetカラムファミリーからフェッチしています。この結果は日時の数字順にソートされます。その際、UserlineがLongTypeの比較子を使用し、reversedがTrueに設定されていることから、順序は降順となります。

ユーザのタイムラインを検索する
start = request.GET.get('start')
limit = NUM_PER_PAGE
 
timeline = TIMELINE.get(useruuid, column_start=start,
column_count=limit, column_reversed=True)
tweets = TWEET.multiget(timeline.values())

前述の例と同じく、ここではツイートIDのリストを検索していますが、今回はTimelineが対象となります。しかし、今回は返されるカラムの範囲を制御するためにstartとlimitを使用しています。これは結果のページングを行うのに便利です。

次のステップ

一般的な考え方について、これで十分理解して頂けていたらと思います。繰り返しますが、簡潔にするためにコードのサンプルの一部を改変し、いくつかの処理を省略しています。したがって、ここでTwissandraのソースをチェックアウトし、より深く調べてみるのも良いかもしれません。リツイートやリストのように、意図的に実装されていないものも数多くありますが、これは入門時のエクササイズとして使えるようにするためです。PythonDjangoで事が足りるのならば、どちらかを試してみても良いでしょう。


wikiに書かれている情報は増えており、そこに含まれる「記事とプレゼンテーション」の一覧も更新されています。


IRCを使っているのであれば、irc.freenode.netの#cassandraに参加できます。ここでは先達とチャットができますし、彼らはいつでも喜んで質問に答えるでしょう。もしEメールの方が好みであれば、cassandra-userリスト上にも助けてくれる人がたくさんいます。

DCIによるWebアプリケーション - 2:アーキテクチャ

前回ドメインレイヤを実装した「割り勘アプリケーション」のプレゼンテーションレイヤを実装し、全体的なアーキテクチャについて考察する。

導入

前回のエントリでは、DCIアーキテクチャの概念に従って「割り勘アプリケーション」のドメインレイヤを実装しました。その後、Wicketを使用してプレゼンテーションレイヤを実装し、「企画」から「会計」までを一通り動かすことができるようにしました。今回はこの作業を通じて得られたDCIアーキテクチャにおける考慮事項について解説していきます。(ソースコードこちらで公開しています。)

※ 配色については、こちらのサイトを参考にさせて頂きました。

DCIとMVC

フロントを含めた基本的なアーキテクチャを以下に示します。

PageクラスとActionクラスの責務分割については、「Pageクラスはコンポーネントの生成と画面遷移のみを行い、ContextおよびRepositoryへのアクセスはActionクラスが行う」方針としています。MVCフレームワークを使って開発している方であれば、特に違和感のない構成だと思います。このように、DCIアーキテクチャMVCモデルの中にすっきりと収まりますが、モデルに対する操作がコントローラ内部に散逸していない点は重要です。なお、ActionクラスがContextではなくRepositoryを直接使用する時の条件については後述します。


例として、AccountActionのコード(抜粋)を示します。

def createMainFormModel(partyId:Int):AccountingMainForm = {
        
    val mainFormModel = new AccountingMainForm
        
    val party:Party = PartyRepository.forPartyId(partyId)
    mainFormModel.partyName = party.name
    mainFormModel.schedule = format.format(party.schedule)
    mainFormModel.location = party.location
    mainFormModel.sum = party.sum.toString

    mainFormModel
}
    
def calculate(partyId:Int, slopeId:Int, 
        paymentListItems:List[PaymentListItem]) = {
        
    val accounting:Accounting = new Accounting(partyId)
    accounting.adjustBy(slopeId)
    accounting.commit
            
    val party:Party = accounting.party
    val participants = party.participants.values
    participants.foreach { participant =>
        val paymentListItem = new PaymentListItem
        paymentListItem.name = participant.name
        paymentListItem.role = participant.role 
        paymentListItem.payment  =
            PartyRepository.paymentOf(partyId, participant.name)
        paymentListItems.add(paymentListItem)
    }
}

コンテキストとデータのライフサイクル

今回プレゼンテーションレイヤを構築したことに伴い、ドメインレイヤのクラス群にも一部修正が入ってます。その中で最大のものは、データのライフサイクルに関するものでしょう。問題となった部分について、以前のテストコードを再掲します。

// id
val partyId = 20
...
// Planning
val planning:Planning = new Planning(partyId)
planning.addPerticipant("Ken", "Chief")
planning.addPerticipant("Taro", "Manager")
planning.addPerticipant("Hanako", "Member")
planning.addPerticipant("Yuji", "Novice")
planning.commit

一見良さそうなのですが、PlanningコンテキストはPartyオブジェクトが生成されるタイミングです。コード内ではpartyIdを渡して初期化していますが、本来はクライアントが恣意的に採番できるものではありません。今回はコンテキスト内部に採番ロジックを移動するように修正しました。

// Planning
val planning:Planning = new Planning
planning.setup("Chris's Birthday", _2010_06_03, "Tokyo")
planning.addPerticipant("Ken", "Chief")
planning.addPerticipant("Taro", "Manager")
planning.addPerticipant("Hanako", "Member")
planning.addPerticipant("Yuji", "Novice")
planning.commit

安易な考慮漏れと言えなくもないですが、ここから得られる教訓もあります。それは「コンテキストはデータのライフサイクルに密接に関わる」ということです。前回はユースケースという視点からのコンテキストの設計に終始しましたが、データオブジェクトがどのタイミングで生成され、どのフィールドがどのタイミングで設定されるのかという観点からも考える必要があります。

まとめ:コマンド-クエリ分離

最初に示したアーキテクチャを再掲します。

問題はActionから伸びる2本の矢印、特にRepositoryに向けられたものをどう考えるかです。これは具体的にはプルダウン内容や一覧の生成、つまりはユーザを特定のコンテキストに導くためのデータ取得ロジックです。「ActionはRepositoryを直接参照しない」というルールを立てるのであれば、これらに対しても何らかのコンテキストを割り当てることは不可能ではありません。しかし、実際にはドメインレイヤの設計がそこまで画面に引きずられるのは好ましいものではありません。この点について考えるにあたり、これまでの議論をふりかえります。

  • DCIはユーザのメンタルモデルを適切にモデリングするための手法である。
  • DCIによってデータの操作に対して適切な意味を与えることができる。
  • コンテキストはデータのライフサイクルと合わせて考えるべきである。
  • コンテキストに縛られず、ユーザをコンテキストに導くためのクエリは必要である。

コンテキストとリポジトリの責務分割を「コンテキストにおいて本来行うべきなのは状態を変化させることであり、リッチなクエリロジックは必要に応じてリポジトリに実装すれば良い」と考えれば、ここにある種の「コマンドクエリ責務分離」(Command and Query Responsibility Segregation:CQRS)を導入することができます(CQRSとはコーディングの原則であるコマンド-クエリの分離をアーキテクチャレベルで適用させるという構想です)。


この戦略を採用することによって、設計はさらにすっきりとしたものになります。実装中に頭をよぎった懸念で、CQRSの導入により解消されたものの例を示します。

  • 「会計士」のadjust処理に「管理者」が登場する必然性がない。
  • 「会計」コンテキストの参照ロジックが、名前から金額を取得するのか、一覧を出すべきなのかなどについてドメインからだけでは方針が立てられない。

これらはさらりと流して実装することもできてしまいますが、「メンタルモデルとの整合性」を突き詰めるには丁寧に考える必要がある問題であり、それが解決されることには重要な意味があると言えます。


ここまで、DCIに従ったドメインモデルをMVCに組み込み、コンテキストの役割を限定した上でCQRS風のアーキテクチャを導入してきました。これで一旦はWebアプリケーションのアーキテクチャの概要が見えたと言えるのではないかと思います。次回は、現在はメモリ上に保存されているデータの永続化を実装し、DCIアーキテクチャに関する考察を深めていきたいと思います。

補足

特にWicket使いである訳でもない私がこのフレームワークを選択した理由としては、プレゼンテーションレイヤの動作だけをサポートしてくれる軽量なフレームワークであり、Scalaでの実装に実績があったことが挙げられます。Wicketでなければならない理由は特にありません。またアプリケーションについて、一通り動くようにはなっていますが、認証、排他制御、入力チェックなどは実装していません。

DCIアーキテクチャによるWebアプリケーションの実装:ドメインレイヤ

DCIアーキテクチャを用いて、割り勘の計算を行うWebアプリケーションを実装する。

導入

前回のエントリでは、DCIアーキテクチャの思想に基づき、試験的にドメインレイヤを実装しました。今回からはその発展編として、DCIアーキテクチャによるWebアプリケーションについて、数回に渡り検討していきます。言語はScala、WebフレームワークとしてはWicketを使用します。なお、データは今のところメモリに保持することとし、永続化層の実装については後日検討することにします。初回の今回はドメイン層をターゲットとして設計について考えていきます。

割り勘アプリケーション

要件

これから実装していくのは「割り勘アプリケーション」です。前回でも使用していたSharePieの概念を身近な形で引き継いだものですね。使用される状況は以下のようなものを想定しています。

  • 飲み会の清算は等分ではなく、役職に応じて「傾斜」をつけることになっている。
  • 例えば:
    • 課長1+新人2で飲みに行って、
    • 飲み代が10000円ならば、
    • 課長が5000円支払い、新人は2500円ずつ支払う。
  • 傾斜の相場は事前に決まっている。
    • この場合であれば、課長:新人=2:1

現実には丸めや端数の処理も必要ですが、おおよその考え方はどこの会社にもあるのではないでしょうか。

アーキテクチャ

レイヤ構成は以下の通りです。

WebアプリケーションのエントリポイントとなるPageクラスからはContextが呼び出され、ContextによってDataとRoleが結びつけられます。Roleによって操作されたDataはRepositoryによって永続化されます。図では表現されていませんが、RoleはDataの操作のみを行い、Repositoryを使用した永続化はContextが行うようにしています。なお、リポジトリとはDDDに登場するドメインの構成要素の一つで、エンティティの永続化を担当するクラスです。

ロールベースのデザイン

コンテキストとロールを意識すると要件は以下のように表現できます。

  1. 準備:役職に応じた傾斜の相場はあらかじめ、管理者が定める。
  2. 企画:企画者が飲み会の参加者を募る。
  3. 支払:店に対する飲み代の支払いは支払者が一括で行う。
  4. 清算:後日、会計士が各参加者の支払い金額を決定する。
    1. 割り勘係管理者に相場を問い合わせ、割り勘表を作成する。
    2. 会計士は割り勘表に従って、各参加者の支払い額を決定する。

ここでは「準備」、「企画」などがコンテキスト、「管理者」、「企画者」がロールに相当します。準備から清算までのコンテキストを一通り流すテストコードは以下の通り。

// id
val slopeId = 10
val partyId = 20

// Preparation
val preparation:Preparation = new Preparation(slopeId)
preparation.addMapping("Chief", 4)
preparation.addMapping("Manager", 3)
preparation.addMapping("Member", 2)
preparation.addMapping("Novice", 1)
preparation.commit

// Planning
val planning:Planning = new Planning(partyId)
planning.addPerticipant("Ken", "Chief")
planning.addPerticipant("Taro", "Manager")
planning.addPerticipant("Hanako", "Member")
planning.addPerticipant("Yuji", "Novice")
planning.commit

// Paying
val paying:Paying = new Paying(partyId)
paying.pay(10000)
paying.commit

// Accounting
val accounting:Accounting = new Accounting(partyId)
accounting.adjustBy(slopeId)
accounting.commit

// verify
val accounting2:Accounting = new Accounting(partyId)
assertEquals(4000, accounting2.paymentOf("Ken"))
assertEquals(3000, accounting2.paymentOf("Taro"))
assertEquals(2000, accounting2.paymentOf("Hanako"))
assertEquals(1000, accounting2.paymentOf("Yuji"))

ドメインレイヤへのエントリポイントはコンテキストですが、これはメンタルモデルとも合致しており、分かりやすいのではないでしょうか。

データモデル

このアプリケーションで使用されるデータモデルを図示します。

「データモデルには一切のロジックが実装されていません」と言いたい所ですが、他のインスタンスから値をコピーするロジックのみ実装されています(理由は後述)。Partyクラスのコードを示します。

class Party(val id:Int) {
    
    var sum:Int = 0
    
    var participants = Map[String, Participant]()
    
    var allot = Map[String, Allot]()
    
    def populateWith(another:Party) = {
        this.sum = another.sum 
        this.participants = another.participants
        this.allot = another.allot
    }
    
}
コンテキスト

データに対するロールのミックスインはコンテキストにおいて行われます。コンテキスト-ロール-データの対応関係を図示します。

コンテキストで行っているのは主に以下の2点です。

  1. データへのロールのミックスイン
  2. ロールのメソッドをキック


Accountingコンテキストのコードを示します。

class Accounting(val id:Int) extends PartyPopulater 
        with SlopePopulater {
    
    val accountant:Accountant = new Party(id) with Accountant 
        with PieMaker
    
    val pieMaker:PieMaker = accountant.asInstanceOf[PieMaker]
    
    val party:Party = populateParty(accountant)    
    
    def adjustBy(slopeId:Int) = 
        accountant.adjust(pieMaker, administrator(slopeId))
    
    def paymentOf(name:String):Int = accountant.paymentOf(name)

    def commit = PartyRepository.add(party)
    
    
    private def administrator(slopeId:Int):Administrator = {
        val administrator:Administrator = 
            new Slope(slopeId) with Administrator
        populateSlope(administrator)
        administrator
    }

ご覧の通り、極めてシンプルです。一点注意があります。Partyクラスがコンテキストに応じて異なるロールを演じるのに対し、言語仕様上、トレイトはインスタンス生成時にしかミックスインできず、自由に外すこともできません。したがって、永続化してあるインスタンスから新しく生成したインスタンスに値を移し替える処理が必要になります。このやや「臭う」コードは、ひとまずXxxPopulaterクラスのpopulateXxxxメソッドにまとめておきます。

ロール

ほとんどのロールは「飲み会」にミックスインされています。DCIアーキテクチャを採用しなかった場合、これらの処理を行うメソッドはイベントに紐づいたクラスに吸収されて見えなくなってしまう(せいぜいプライベートメソッドとして切り出される程度)か、「飲み会」クラスが肥大化してしまうか、処理を委譲するための技巧的なクラスが登場していたか、おおよそこのいづれかだったのではないでしょうか。DCIアーキテクチャScalaで実装した場合であれば、トレイトと抽象メンバを使用することによって、データを隠蔽しつつクラスの責務を明確に分割することができます。


Accountantクラスの実装を示します。

trait Accountant {
    
    var sum:Int
    
    var allot:Map[String, Allot]
        
    def adjust(pieMaker:PieMaker, administrator:Administrator) = {
        val pie = pieMaker.createPie(administrator)
        this.allot = calculateAllot(pie)
    }
    
    def paymentOf(name:String) = allot(name).amount
    

    private def calculateAllot(pie:Map[String, Fraction]):
            Map[String, Allot] = {
        var tempAllot = Map[String, Allot]()
        for (it <- pie) {
            val name = it._1
            val payment:Int = pie(name) * sum
            tempAllot += (name -> new Allot(name, payment))            
        }
        tempAllot
    }
            
}

ここで、pieとは「参加者の名前」をキー、「自分の重み/参加者全員の重みの合計」を値としたマップになっています。要件では「割り勘表」と表現していたこのマップを作るのが「割り勘係 / PieMaker」の役割、割り勘表を元に各人の支払い金額を計算するのが「会計士 / Accountant」の役割です(なお、Fractionクラスは分数を表現するために独自に実装したもので、*メソッドを持っています)。

まとめ

実は「会計士」と「割り勘係」という役割分担はアップフロントに設計できていたものではなく、数回リファクタリングをした結果に出てきたものです。そういった経緯を経て概ね以下のような指針に従うと良いのではないかという気がしています。

  1. ロールの分割はシナリオの登場人物として記述できる単位であることが望ましい。
  2. データモデルの特定のフィールドに対する更新は特定のロールに集めるべき。
  3. ロールは別のロールとやり取りするために自分が抽象メンバとして持っているフィールドを渡すべきではない。
    • もう一方のロールも同じデータクラスにミックスインし、必要なフィールドを抽象メンバとして宣言すれば良い。


一点懸念も付け加えておきます。Accountingコンテキストのコードの一部を再掲します。

def adjustBy(slopeId:Int) = 
    accountant.adjust(pieMaker, administrator(slopeId))

今のところ、「コンテキストの1メソッドで呼び出すのはあるロールのトリガメソッドを1つだけ」というルールに従っています。論文にも「コンテキストにとってエントリとなるロールのトリガメソッドをキックする」とあることから、DCIアーキテクチャとしては正しいことだと思うのですが、ロールの数が増えると多少ごちゃごちゃしそうな気配もあります。この問題について考える時のポイントは、DCIアーキテクチャの目的が「ユーザのメンタルモデルに従うこと」にあると考えています。ロールの分割に関するルールの最初にシナリオとの整合性をあげたのは、その混乱を極力メンタルモデルと整合させるためでもありました。


なお、今回使用したサンプルは、すべてこちらで公開しています。

次回:Scala-Wicket-Maven-Jetty

次回はWicketでプレゼンテーションレイヤを実装し、今回の実装と組み合わせてとりあえず動くものを作りたいと考えています。Scala-Wicketに興味がある方のため、さしあたりこの資料を参考にしてHelloWorldレベルのサンプルを作成し、タグを切りました(Eclipse 3.5.2)。お役に立てば幸いです。