事件源多对多聚合

Event Sourced many-to-many aggregates

我们有一个事件源系统,出于性能原因,我们将数据缓存到聚合中。

假设我们有 3 个实体。病人,医生,预约。

适合我们的是一对多类型的关系。例如,想象以下事件:

* DOCTOR_CREATED
* DOCTOR_ARRIVED
* DOCTOR_LEFT

这我们可以聚合成一对多类型的关系。当一个 Doctor 添加到系统中时,我们可以在数据库中创建一行。

每次他们上班时,我们都可以将那个时间添加到医生记录中,每次他们离开时,我们也可以将那个时间添加到医生记录中。所以我们可以得到这样的结果:

{
  "id": 23,
  "name": "Dr Bill",
  "totalHoursWorked": 124,
  "timesheet": [
    {
      "arrivedAt": "2022-01-04 09:00:00",
      "leftAt": "2022-01-04 14:00:00",
      "hoursWorked":  5,
    },
    // etc...
  ]
}

没问题。

现在假设我们要跟踪约会。这是用户和医生之间的多对多关系。

我对这些活动感兴趣:

* DOCTOR_CREATED
* PATIENT_CREATED
* APPOINTMENT_CREATED

因为事件流必须按时间顺序排列,所以我无法在创建相关医生或患者之前创建预约记录。

如何从约会的角度创建数据模型的视图。

也许用 graphql 术语考虑它可能会有所帮助,但我想优化此查询:

query {
  appointment {
    day
    time
    patient {
      name
      age
    }
    doctor {
      name
      specialty
    }
  }
}

我希望能够将此数据结构作为聚合存储在数据库中。这样就可以在一个数据库查询中完成通过 ID 获取预约。

我要 运行 解决时间轴的问题,因为如果我在数据库中创建行之前等待第一个 APPOINTMENT_CREATED 事件,那么我就错过了相关的患者和医生事件。

如果我在预期中首先捕获 PATIENT 和 DOCTOR 事件,那么我必须存储 Doctor 和 Patient 的所有可能组合,以防万一他们中的一个可能想稍后预约。我还面临着具有不一致数据结构的聚合问题。 table 中的行可能由医患 ID 或预约 ID 索引,具体取决于我们到达的事件流的哪个阶段。

目前我能想到的唯一方法是让尝试优化此查询的聚合等待 APPOINTMENT_CREATED 事件,然后必须异步查询数据库以检索患者和医生当时的记录。

虽然我们实现系统的方式,但我们所有的聚合都是由纯函数的组合构建的,这些纯函数只采用先前的聚合状态、相关事件和 return 新的聚合状态。

我们目前构建的体系结构是否无法实现我想要的?我是否需要一个逃生口来让我们的聚合水合作用执行异步数据库查询(不热衷于此)?

或者聚合是解决这个问题的错误技术,我实际上需要使用其他东西(比如缓存)。尽管这么说,但事件源给我带来的好处之一是我们不必为缓存而烦恼,因为我们可以预先构建所有聚合以针对前端进行读取优化。

我可以明确地说,事件溯源并没有带来不必为缓存而烦恼的好处,它也与 pre-building 聚合没有任何关系,read-optimized 用于前端结尾。 Read-optimized 数据模型是 CQRS,它与事件溯源完全不同(两者都不需要另一个,尽管它们非常适合)。

CQRS/ES 系统中的 read-side 通常不会 event-sourced:您在 event-sourcing 的同时执行 CQRS 的原因是 event-sourced 模型对于查询来说很糟糕,因此您正在实施非 event-sourced 数据模型(例如关系或 NoSQL 文档...)以允许有效查询。

所以不要觉得您需要在 read-side 中使用与在 write-side 中使用的结构相同的结构。 Event-sourcing 与 write-side 具有相同的聚合体是特别不可能有益的。

如果您确实在 read-side 中使用了 event-sourced 聚合,请注意它们将不同于 write-side 中的聚合。例如,构建约会的非规范化视图的 过程 可能非常适合 event-sourced 聚合:

  • 在一个 APPOINTMENT_CREATED 事件(可能包括医生 ID 和患者 ID)上记录该过程开始构建非规范化视图
  • 该“开始构建...”事件的订阅者然后为适当的医生和患者重播事件(任何事件存储都应支持这一点:为特定聚合重播事件是事件溯源中的规范操作)并使用这些事件为流程聚合构建一个“这是医生(resp.patient)信息”的命令
  • 聚合记录找到的信息,甚至可能记录“找到所有内容”事件
  • 另一个订阅者正在构建要保存的非规范化视图,以便通过约会 ID 轻松查询

请注意,在此,“非规范化过程”聚合是纯的;副作用发生在其事件的预测中。

我会在实践中这样做吗?可能不会。我可能会使用关系数据库并让读者进行连接。我可能只是让约会创建的事件启动异步查询以构建非规范化视图。尽管如此,这种事情还是有可能的。