参考
AggregateRoot
聚合根参考:Root 接口、嵌入 aggregate.Base、纯状态 applier、Apply、不变量、唯一键、分区键与未提交事件。
AggregateRoot
包:aggregate
Aggregate(聚合根)是一致性边界(同一聚合内业务规则要对齐):状态等于对其事件流的折叠。命令通过调用聚合方法来改状态——方法先检查规则,再 Apply 事件。
接口
type Root interface {
ID() uuid.UUID
AggregateType() string
UncommittedEvents() []event.Event
ClearUncommitted()
Version() int64
LoadFromHistory(events []event.Event) error
Invariants() error
UniqueKeys() map[string]any
PartitionKey() string
}
这些方法几乎不用手写。嵌入 aggregate.Base 就全有了。Invariants 默认返回 nil;UniqueKeys 返回你用 SetUniqueKeys 存进去的内容——需要时再自己覆盖。对没注册 applier 的事件调用 Apply 会 panic。
规则
- Applier 只做纯状态变更。 按事件改字段即可:不做 I/O,不读时钟,不用随机值。
- 命令方法先校验,再
Apply。 一次调用要么返回错误且不产生事件,要么产生一个或多个事件。 - 做决策时不读其他聚合。 只用自身状态和传入参数。
规则 1 保证回放正确:从历史加载出来的状态,必须和当初线上产生这段历史时一致。
嵌入 aggregate.Base
type Account struct {
aggregate.Base
Owner string
Currency string
Balance types.Money
Open bool
}
func NewAccount(id uuid.UUID) *Account {
a := &Account{}
a.Init(id, "bank.Account")
a.On(&AccountOpened{}, func(e event.Event) {
ev := e.(*AccountOpened)
a.Owner, a.Currency, a.Balance, a.Open = ev.Owner, ev.Currency, ev.Opening, true
})
return a
}
Init(id, typeName)设置聚合 id 与类型名。在构造函数里、任何Apply之前调用。On(sample, applier)按sample的 Go 类型注册 applier,传入空事件结构体的指针。Apply(event)跑 applier,填上事件的 aggregate id、序号和时间,并追加到未提交列表。Version()是最后已提交的序号,也是下次追加时的ExpectedVersion。
命令方法
func (a *Account) Deposit(amount types.Money) error {
if !a.Open {
return fmt.Errorf("account not open")
}
if amount.IsZero() || amount.IsNegative() {
return fmt.Errorf("deposit amount must be positive")
}
if amount.Currency != a.Currency {
return fmt.Errorf("currency mismatch")
}
a.Apply(NewMoneyDeposited(amount))
return nil
}
违反业务规则就返回错误。失败的方法不会在未提交列表里留下任何东西,事务也会回滚。
不变量
Invariants() error 表达状态必须一直成立的规则。Repository 在 Load 之后检查一次,追加前对每个有未提交事件的聚合再检查一次。
func (a *Account) Invariants() error {
if a.Open && a.Balance.IsNegative() {
return fmt.Errorf("invariant: balance negative")
}
return nil
}
唯一键(Unique keys)
UniqueKeys() 返回 scope → value 的映射。store 在追加时强制唯一,冲突时返回 ErrUniqueKeyViolation。Base 提供 SetUniqueKeys,聚合可在 applier 里刷新键。scope 里要带上租户。见 唯一键与 OCC。
分区键
PartitionKey() 返回随聚合存储的分区提示。多租户系统里通常由租户派生:
acc.SetPartitionKey(tenancy.PartitionKey(tenantID))
用途包括:在共享 schema 内对 events 做物理分区,以及租户级唯一键。见 Tenancy(多租户)与 分区。
未提交事件
Apply 产生的事件留在 UncommittedEvents() 里,直到 Repository.Commit 追加。提交成功后,Repository 会清空列表并推进聚合版本,不用你自己清理。
快照
聚合实现 SnapshotVersion、MarshalSnapshot、UnmarshalSnapshot 即可启用 JSON 快照。见 Snapshotting。
并发
不要在 goroutine 之间共享聚合实例。每次 Execute 有自己的 UnitOfWork,里面加载的聚合只属于这次调用。
测试
testkit.For(id, factory).Given(events...).When(fn).Run() 从事件重建聚合并捕获方法产出。见 测试。
相关
文档随 go-sequent 源码更新;若与 API 不一致,以源码为准。