参考
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 的步骤:
- 若已在跟踪该实例,直接返回。
- 查找该聚合类型的工厂;没有则报错:
aggregate: no factory for type。 - 若设置了
SnapshotStore,加载版本与SnapshotVersion()匹配的最新快照,恢复状态,并把聚合版本设为快照序号。 - 通过
Store.LoadStream加载事件流,跳过快照序号及之前的事件。既没有事件流也没有快照时返回ErrStreamNotFound。 - 用
LoadFromHistory回放剩余事件。 - 检查
Invariants()。 - 在 UoW 中跟踪该聚合。
LoadStream 读取时会校验 hash chain(按聚合串联事件哈希,用来发现篡改),因此篡改会在这里以 ErrHashChainTamper 暴露。
Commit
每条命令在 Handler 返回后执行一次 Commit:
- 对每个有未提交事件的被跟踪聚合运行
Invariants()。 - 为每个聚合构造
StreamAppend:id、类型、PartitionKey、ExpectedVersion: agg.Version()、事件、Unique keys。 - 调用
Store.Append(ctx, commandRecord, streams),原子写入命令行与所有事件流;OCC(用 ExpectedVersion 挡住并发写冲突)与唯一键在此强制执行。 - 按加载顺序、再按序号,把全部事件交给
Publisher.Publish。使用 SyncInTxn(在写事务里同步更新读模型)时,Projector(把事件写成查询用读模型的组件)与 Workflow 在这里于事务内运行。 - 标记每个聚合已提交:版本前进,未提交列表清空。
任何一步失败,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 不一致,以源码为准。