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アーキテクチャが提供するロールやコンテキストといった概念は、そうした試みにおける「思考のフレームワーク」としておおいに役立つものなのではないでしょうか。