我如何设计从遗留的面向 CRUD 的应用程序到 CQRS 和事件源系统的桥梁?

How can i design a bridge from a legacy CRUD oriented app to a CQRS and Event sourcing system?

我被要求在遗留 Web 应用程序中实施 CQRS/Event 采购模式,以便准备将其从 monolithic/state 面向模型迁移到分布式、面向服务的应用程序。

我有一些关于如何设计面向域的代码包的问题,​​该代码包将强耦合到数据库的遗留实体与新的事件源模型连接起来。

我做的第一件事是:

  1. 为 CQRS/ES 写一个小 "framework",类 如 AggregateRoot、DomainEvent、Command、Handlers、Messaging、Eventstore、AggregateIds 等
  2. 尝试将遗留实体分组并"migrate"到一些聚合中,以将应用程序的所有历史和状态重建到 EventSoourced 聚合中
  3. 在旧控制器中插入一些命令调度,以便让应用程序按原样工作,同时也为新的 CQRS/ES 系统提供支持。

上下文:

遗留应用程序包含几个实体,映射到数据库,包含模型层。 (我们的领域是人力资源(人力)。 假设我们有这些现有实体:

  1. Worker,具有各种字段和相关实体(OneToOne,OneToMany),如

    • 姓名
    • 地址 1-1
    • 能力 1-N
  2. 社会,工人工作的社会,有各种领域和相关实体(OneToOne,OneToMany),如

    • 姓名
    • 地址 1-1
    • 小时
  3. 合同,具有各种字段和相关实体(OneToOne,OneToMany),如

    • 地址 1-1
    • 工人 1-1
    • 社会1-1
    • 文档 1-N
    • 第 1-N 天
    • 小时
    • 等等

根据这个遗留模型,我设计了一个 MissionAggregate 包含:

我还设计了一个 WorkerAggregate 和一个 SocietyAggregate,带有字段和 UUIDS,在 MissionAggregate 中我添加了:

正如我之前所说,我的目标是保留旧应用程序原样,但只是在 CRUD 控制器的方法中引入一些调用以将命令分派到新的 CQRS 系统。

例如:

在 bdd 中刷新新创建的 Contrat 后,我​​想将 "CreateMissionCommand" 调度到新的命令总线。

它以适当的命令处理程序为目标,该处理程序处理所有命令的数据,将其传递给具有新 UUID 的新创建的聚合,并将 "MissionCreatedDomainEvent" 存储在 EventStore 中。

DomainEvent 使用 AggregateId、播放头进行索引,并具有包含应用和构建 MissionAggregate 所需字段的有效负载。

在应用程序中创建的新合同现在像往常一样具有以前的生命周期,以及旧版应用程序对其进行的所有更新。但我还需要将所有这些更改反映到相应的 EventSourcedAggregate,因此每次应用程序中的数据库刷新时,我都会发送一个命令,将遗留应用程序的 "crud like operations" 转换为面向域/面向命令的模式.

总结一下工作流程就是:

  1. 发生 Crud 遗留操作并刷新合约实体上的一些更改
  2. 在控制器的一行代码中,我将一个用必要字段构建的命令(MissionAggregate 的 AggregateId...我需要存储在某处...请参阅下一个问题)发送到域命令总线,因此对现有代码库的影响非常小。
  3. 总线将命令传递给相应的命令处理程序
  4. 处理程序加载聚合并通过调用适当的聚合方法应用更改
  5. 然后经过一些验证,聚合引发并存储适当的事件

我的问题和问题(至少有一些 ;))是:

  1. 我觉得我正在重写遗留应用程序的所有大部分,聚合之间的关系与实体之间的关系相同,并且具有相同类型的验证、检查等.

  2. 在 MissionAggregate 中同时引用 WorkerAggregate 和 SocietyAggregate UUID 意味着我还必须构建这些聚合(因此在刷新 Worker 和 Society 实体时从遗留应用程序调度命令)。我不能只引用 Worker 的实体 ID 和 Society 的实体 ID 吗?

  3. 如何避免 MissionAggregate 不断增长?合同实体非常庞大,它有大量不断更新的字段(小时、天、文件等)。如果我想存储所有这些事件,我需要一个大的 MissionAggregate 来反映所有这些变化;所以我需要大量的 CommandHandlers 来响应我要从遗留应用程序发送的所有添加、更新等命令。

  4. "free" 如何从它应该引用的根实体中生成聚合?例如,合同实体需要在某处与其相关的任务聚合相关联,例如当我想从应用程序发送命令时,就在遗留代码刷新实体上的某些内容之后。在哪里存储这种关系?在实体本身中,在 AggregateId 字段中?在聚合中,我应该有一个 ContratId 字段吗?或者我应该在某个地方有某种映射 Table 来保存合同 ID 和 MissionAggregate ID 之间的关系?

  5. 如何处理过去?我是否应该通过一个脚本迁移所有现有数据,该脚本在所有历史数据上生成聚合和事件?

提前感谢您的宝贵时间。

你面前有一个巨大的任务,让我们试着分解它。

最好将系统的这个新部分与遗留代码库隔离开来,否则你将在每一个环节都束手无策。

在您的项目中为这些新要求创建一个单独的层。从现在开始我们称它为“泡沫”。这个泡沫将像一个绿地项目,有自己的结构、依赖关系等。泡沫和遗产之间没有直接的联系;通信将通过另一个专用转换层进行,我们将其称为“反腐败层”(ACL)。

ACL

这就像两个系统之间的API。

它将调用从气泡转换为旧版,反之亦然。其目的是防止一个系统破坏或影响另一个系统。这样您就可以 building/maintaining 每个系统彼此独立。

同时,ACL 允许一个系统使用另一个系统,并重用逻辑、验证、规则等。


直接回答您的问题:

  1. I feel like i am rewriting all big portions of the legacy app, with the same kind of relations between the Aggregates that i have between the Entities, and with the same type of validations, checks etc.

使用 ACL,您可以求助于调用验证并重用遗留代码的实现。这将使您有时间根据需要或尽可能重写内容。

不过,您可能不需要重写整个系统。如果你的目标是实施 CQRS 和事件溯源,并且你可以通过保留大部分或部分遗留系统来实现这个目标,我会说你会这样做。当然,除非目标之一是完全取代旧系统。否则,保留它;尽量少写代码。

建议的工作流程:

  • 将 CQRS 和事件溯源系统保持在气泡中
  • 不要将这些新框架纳入旧框架
  • 使滞后控制器发出对 ACL 的方法调用
  • ACL 会将这些调用转换为命令并分发它们
  • 任何事件都将被您的事件溯源框架捕获
  • 结果将保存到气泡的数据库中

气泡的数据库可以是同一数据库中的不同模式,也可以是完全不同的数据库。但是您必须考虑同步,这是它自己的主题。为了降低复杂性,我建议在同一个数据库中使用不同的模式。

  1. Having references, to both WorkerAggregate and SocietyAggregate UUID in MissionAggregate implies that i have to build those aggregate also (hence to dispatch commands from legacy app when the Worker and Society entities are flushed). Can't i have only references to Worker's entity id and Society's entity id?

  2. How can i avoid having a eternally growing MissionAggregate ? The Contract Entity is quite huge, it has a looot of fields that are constantly updated (hours, days, documents, etc.) If i want to store all those events, i need to have a large MissionAggregate to reflect all those changes; and so i need to have a tons of CommandHandlers that react to all the Commands of add, update, etc that i am going to dispatch from the legacy app.

你应该以小聚合为目标。巨大的聚合可能会降低性能并导致并发问题。

如果您预计会有一个巨大的聚合,最好重新考虑它并尝试将其分解。问什么 fields/properties 一起变化 - 这些可能是不同的聚合。

此外,当您谈到 CQRS 时,您通常倾向于在系统中以基于任务的方式做事。

想想传统的 Web 应用程序,其中有一个包含许多字段的巨大页面,当用户保存时,这些字段全部发送到服务器。

现在,将其与用户在每个步骤中更改一小部分数据的现代 Web 应用程序进行对比。如果您以这种方式考虑您的系统,您会发现那些较小的聚合体。

PS。您不需要为此重建您的界面。如果您的遗留系统有那些大页面,您可以在控制器中使用逻辑来检测更改了哪些字段并发出适当的命令。

  1. How "free" is an Aggregate from the Root entity it is supposed to refer to ? For example, a Contract Entity needs to relate somewhere to it's related Mission Aggregate, like for example when i want to dispatch a Command from the app, just after the legacy code having flushed something on the Entity. Where to store this relation ? In the Entity itself, in a AggregateId field ? in the Aggregate, should i have a ContratId field ? Or should i have some kind of Mapping Table somewhere that holds the relationship between Contract ID and MissionAggregate ID?

聚合代表一个概念整体。它们就像原子,不可分割的东西。您应该始终通过其根实体 ID 引用聚合,而不是子实体 ID:从外部看,没有子实体。

聚合应该作为一个整体加载并作为一个整体持久化。拥有小聚合体的另一个原因。

聚合可以由单个实体组成。或者它可以有更多的实体和值对象,形成一个图,但一个实体将被选为根,并将持有对其子实体的引用。子实体和值对象不应持有对其父实体的引用。依赖不是双向的。

如果 Contract 是 Mission 聚合内的实体,则 Contract 不应引用其父项。

但是,如果您的 Contract 和 Mission 是不同的集合,那么它们可以通过 ID 相互引用。

  1. What to do with the past? Should i migrate all the existing datas through a script that generates Aggregates and events on all the historical data?

这是业务专家的问题。他们需要吗?如果他们不这样做,那么不要仅仅为了这样做而实施它。考虑到成本和权衡,您做出的每个决定都应着眼于满足业务需求并为其创造实际价值。

有人说代码是一种负担,而不是一种资产,我在某种程度上同意:你写的每一行代码都需要经过测试和支持。不要写任何不是真正必要的代码。


另外,看看这个 article about the Strangler Pattern,它展示了如何通过用新的应用程序和服务逐渐替换特定功能来迁移遗留系统。

如果有机会,请在 Pluralsight 观看此课程(付费):Domain-Driven Design: Working with Legacy Projects。作者提出了处理此类任务的实用方法。

我希望这能给你一些见识。

我不想破坏你的游戏。每个人都知道从头开始重写一些东西是多么酷。这是一个挑战,它很有趣,它令人兴奋。然而...

migrate it from a monolithic/state oriented model to a distributed, service oriented app

CQRS/Event 采购不会解决 任何 您的问题,也不会帮助您以任何合理的方式分发应用程序。如果你只是在 CRUD 操作上生成事件,你将在每个部分之间有大量混乱的依赖关系。每个需要数据的部分都必须调用几个 "services"(即表)来获取它,而不是将数据推送到其他地方,生成一些其他部分会做出反应的事件 1。这将是一团糟。通常这被称为 分布式整体

这也是您已经看到它存在问题的原因。这些问题不会消失,因为您本质上是在以相同的方式构建相同的系统,但这次会更复杂。

从这里去哪里

第一件事永远是:有一个明确的目标。你想要一个你说的面向服务的架构。为什么?是否有部分需要不同的缩放比例、不同的资源?它们是否由具有不同生命周期的不同团队管理? ETC。?也许你已经拥有了这一切,我不知道,但如果没有,那是你的第一个任务。

然后。您 想要提取的部分不能只是 CRUD 东西。这些不会是独立的,所以无论你的目标(见上文!)是扩展还是不同的团队,你都不会实现你的目标!要独立,您必须使用数据提取 行为 ,并且 服务可以自行运行

您不能只是抛出流行语并希望得到最好的结果。我建议忽略所有的炒作和流行语,想想你想要达到的目标。

例如:我需要 100 万名员工在 10 分钟内记录他们的时间。所以这意味着我需要一个 "service" 来使工作人员能够使用 Web 界面记录他们的时间。因此,让我们将其创建为具有自己数据库的完整独立部分,以便在需要时可以将其扩展到 100 个节点。每隔一小时左右自动导出数据到billing。