跳到正文

参考

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 不一致,以源码为准。