State Sourcing(CRUD)
なら以下のようなイメージ。まぁ普通によく見るロジック。
class AddCartItemCommandProcessorOnSS(cartRepository: CartRepository) {
def execute(cartId: CartId, itemId: ItemId, num: ItemNum): Unit = {
// 最新の集約(グローバルなエンティティ)をストレージから取得する
val cart = cartRepository.findById(cartId)
// ロジック実行: 予算超過ならカートオブジェクトが商品の追加を拒否する! (1)
val newCart = cart.addItem(itemId, num)
// 更新された最新状態をストレージに保存する
cartRepository.store(newCart) // 必要なら楽観ロックなどを行う (2)
}
}
Event Sourcing
だと以下のようになる。(1)の整合性チェックは可能だが、(2)のロックはできなくなる…。複数のサーバやスレッドでこのメソッドが呼ばれてしまうと不正なイベントがキューに登録されてしまうことがある。
class AddCartItemCommandProcessorOnES(cartEventService: CartEventService) {
def addCartItem(cartId: CartId, itemId: ItemId, num: ItemNum): Unit = {
val allEvents = cartEventService.getAllEventsById(cartId)
val cart = new Cart(allEvents)
// ロジック実行: 予算超過ならカートオブジェクトが商品の追加を拒否する! (1)
val itemAdded = cart.addItem(itemId, num)
// 複数サーバで実行されても大丈夫? いや大丈夫ではない。壊れます!
// 一般的なキューだと楽観ロックはできません。かといってRDBも不向き
cartEventService.publish(itemAdded) // イベントストレージに追記書き込み。常に追記になります。(2)
}
}
Cart#addItem
が、システム全体でただ一つの一意な軽量プロセスで処理されるなら、Cart#version
を使って楽観ロックを実現することが可能です。
class AddCartItemCommandProcessorOnES(cartEventService: CartEventService) {
def addCartItem(cartId: CartId, itemId: ItemId, num: ItemNum): Unit = {
val cart = actorSystem.actorOf(cartId) // システム全体でただ一つの軽量プロセスとしてのCartの参照を取得する
// コマンドのバージョンとCartのバージョンが一致していなければ拒否する!
// ロジック実行: 予算超過ならカートオブジェクトが商品の追加を拒否する!
val itemAdded = cart.addItem(itemId, num)
cartEventService.publish(itemAdded) // イベントに現在のバージョン+1を格納する
}
}