跳到正文

参考

Repository 与 UnitOfWork

聚合 Repository 与挂在 context 上的 UnitOfWork:Add、Load、Commit、identity map、工厂注册、快照加载与 goroutine 使用约定。

Repository 与 UnitOfWork

包:aggregate

Repository 负责加载聚合、在 UnitOfWork 里跟踪它们,并通过 EventStore 与 Publisher 提交未提交事件。

构造与工厂

repo := aggregate.NewRepository(store, publisher)
repo.RegisterFactory("bank.Account", func(id uuid.UUID) aggregate.Root {
    return NewAccount(id)
})

工厂用来构造指定类型的空聚合。Load 重建聚合需要它,快照重建也需要。用门面时,app.RegisterAggregate(type, factory) 会代劳。

字段:Store、Snapshots(可选)、Publisher、Factories。

UnitOfWork

UnitOfWork 挂在一次 Execute 的 context 上,本质是 identity map 加上有序的聚合列表。

  • Add 与 Load 需要 context 上有 UoW,否则返回 aggregate: no UnitOfWork in context。
  • id 已知时,Load 返回 UoW 里已有的实例。同一次 Execute 不会得到同一聚合的两份可变副本。
  • Commit 按加载顺序收集被跟踪聚合的未提交事件。
  • Execute 里的 defer uow.Clear() 在调用结束时清空它,成功失败都一样。

不要跨 goroutine 共享 UoW 或其持有的任何聚合。并行应当发生在请求之间,而不是请求内部。

Add

acc := NewAccount(id)
if err := acc.OpenAccount("alice", "CNY", opening); err != nil {
    return err
}
return repo.Add(ctx, acc)

Add 跟踪一个新聚合,其事件在提交时落库。

Load

agg, err := repo.Load(ctx, id, "bank.Account")

Load 的步骤:

  1. 若已在跟踪该实例,直接返回。
  2. 查找该聚合类型的工厂;没有则报错:aggregate: no factory for type。
  3. 若设置了 SnapshotStore,加载版本与 SnapshotVersion() 匹配的最新快照,恢复状态,并把聚合版本设为快照序号。
  4. 通过 Store.LoadStream 加载事件流,跳过快照序号及之前的事件。既没有事件流也没有快照时返回 ErrStreamNotFound。
  5. 用 LoadFromHistory 回放剩余事件。
  6. 检查 Invariants()。
  7. 在 UoW 中跟踪该聚合。

LoadStream 读取时会校验 hash chain(按聚合串联事件哈希,用来发现篡改),因此篡改会在这里以 ErrHashChainTamper 暴露。

Commit

每条命令在 Handler 返回后执行一次 Commit:

  1. 对每个有未提交事件的被跟踪聚合运行 Invariants()。
  2. 为每个聚合构造 StreamAppend:id、类型、PartitionKey、ExpectedVersion: agg.Version()、事件、Unique keys。
  3. 调用 Store.Append(ctx, commandRecord, streams),原子写入命令行与所有事件流;OCC(用 ExpectedVersion 挡住并发写冲突)与唯一键在此强制执行。
  4. 按加载顺序、再按序号,把全部事件交给 Publisher.Publish。使用 SyncInTxn(在写事务里同步更新读模型)时,Projector(把事件写成查询用读模型的组件)与 Workflow 在这里于事务内运行。
  5. 标记每个聚合已提交:版本前进,未提交列表清空。

任何一步失败,Commit 返回错误,Execute 回滚事务。没有未提交事件时,Commit 什么也不做。

事件顺序是:先按聚合的加载顺序,再按各聚合内部的序号。它不取决于跨聚合交错的 Apply 调用时机。

Exists

ok, err := repo.Exists(ctx, id)

报告某个事件流是否已有事件。

常见错误

错误 来源
eventstore.ErrOptimisticConcurrency Append:流末尾与 ExpectedVersion 不符
eventstore.ErrUniqueKeyViolation Append:唯一键已被占用
eventstore.ErrStreamNotFound Load:未知的聚合 id
eventstore.ErrHashChainTamper Load:已存储事件校验失败
不变量错误 你的 Invariants(),在 Load 之后或追加之前

相关

文档随 go-sequent 源码更新;若与 API 不一致,以源码为准。