参考
Projector
go-sequent Projector:ManagedTables、SyncInTxn 更新余额读模型、可重放的 SQL Persistor、EventRouter 与重建规则。
Projector
包:projector
Projector 把事件写成查询用读模型——最常见的就是余额表。在 go-sequent 里,账本类读模型是在写事务里更新的(SyncInTxn),不是事后异步慢慢追。
接口
type Projector interface {
publish.Handler // Handles() []string;Handle(ctx, event.Event) error
ManagedTables() []string // 该 projector 拥有的表
}
ManagedTables 声明这张(或这些)表归谁管。读模型重建流水线会据此创建、填充并切换表。
通过门面注册:
app.RegisterProjector(NewBalanceProjector())
Projector 跑在 SyncInTxn 上:事件 Append 之后、事务提交之前。返回错误会回滚整次写入。见 Publisher。
Bank 示例中的 Projector
func (p *BalanceProjector) ManagedTables() []string { return []string{"account_balances"} }
func (p *BalanceProjector) Handles() []string {
return []string{
EventAccountOpened, EventMoneyDeposited, EventMoneyWithdrawn,
EventTransferDebited, EventTransferCredited,
}
}
func (p *BalanceProjector) Handle(ctx context.Context, e event.Event) error {
switch ev := e.(type) {
case *AccountOpened:
p.balances[ev.AggregateID()] = ev.Opening
case *MoneyDeposited:
return p.add(ev.AggregateID(), ev.Amount)
case *TransferDebited:
return p.sub(ev.AggregateID(), ev.Amount)
// ...
}
return nil
}
这个演示把余额放在内存里。生产环境请用 SQL。
Persistor
projector.Persistor 是 Projector 应当使用的唯一 I/O 入口:
type Persistor interface {
Exec(ctx context.Context, sql string, args ...any) error
QueryRow(ctx context.Context, sql string, args ...any) Row
}
Postgres 版 Persistor 会加入当前写操作已开启的事务:和事件追加同一条连接,回滚时读模型写入也会一起撤销。重建时流水线可以把同一个 Projector 指到另一张临时表,所以重放是可行的。
始终用参数化 SQL,不要用事件数据拼字符串。
examples/bankpg 里的 SQLBalanceProjector 就是这样写 bank_account_balances 的。见 Bank 示例。
EventRouter
按事件类型分派时,可以用 projector.NewEventRouter(tables),少写一大段 type switch:
r := projector.NewEventRouter([]string{"account_balances"})
r.On("bank.MoneyDeposited", func(ctx context.Context, e event.Event) error {
// 更新行
return nil
})
app.RegisterProjector(r)
Handles() 会根据你用 On 注册的类型自动得出。
账本 Projector 的规则
- 一张表一个 Projector。 每张托管表只由一个 Projector 写入。
- 失败必须让写操作失败。 不要吞错误。静默跳过某次入账,余额就错了。
- 确定性。 同一串事件,线上跑和重建重放,结果行必须一样。
- 按租户隔离。 读模型表带
tenant_id,查询用WHERE tenant_id = $1。 - 读模型表里不要有跨聚合外键。只保证单个聚合内的事件顺序。
- 不做外部调用。 离开数据库的事交给 Workflow 钩子或 Outbox。
重建
读模型表可以丢掉,再从 EventStore 重放重建。流水线是 prepare → replay → catchup → optimize → golive,另有 abort。重放用 PublishProjectorsOnly,所以 Workflow 不会跑。见 读模型重建。
重建完成后,线上路径的余额仍然以 SyncInTxn 为准。
相关
文档随 go-sequent 源码更新;若与 API 不一致,以源码为准。