【发布时间】:2017-04-10 22:27:33
【问题描述】:
澄清一下:BuckupableThing 是一些硬件设备,其中写入了程序(已备份)。
更新说明:这个问题更多的是关于 CQRS/ES 的实现,而不是关于 DDD 建模。
假设我有 3 个聚合根:
class BackupableThing
{
Guid Id { get; }
}
class Project
{
Guid Id { get; }
string Description { get; }
byte[] Data { get; }
}
class Backup
{
Guid Id { get; }
Guid ThingId { get; }
Guid ProjectId { get; }
DateTime PerformedAt { get; }
}
每当我需要备份 BackupableThing 时,我需要先创建新项目,然后创建新的备份,并将 ProjectId 设置为这个新项目的 ID。只要每个新备份都有新项目,一切都会正常工作。
但如果项目不存在,我真的需要创建项目,现有项目的唯一 ID 应该是它的 Data 属性(某种字节 [] 数组的哈希)。因此,当备份任何其他 BackupableThing 并且系统发现另一个 BackupableThing 已备份且具有相同的结果(数据)时 - 显示已创建且正在运行的项目以及所有描述和所有设置。
首先我想通过某种方式在 Guid 中编码哈希来解决这个问题,但这似乎很笨拙且不简单,而且它还增加了与随机生成的 Guid 发生冲突的机会。
然后我想出了一个单独的表(带有单独的存储库)的想法,它包含两列:数据哈希(一些 int/long)和 PlcProjectId(Guid)。但这看起来很像投影,实际上是一种投影,所以理论上我可以使用 Event Store 中的域事件重建它。我读到从域服务/聚合/存储库(从写入端)查询读取端是不好的,而且我在一段时间内想不出别的东西。
更新
所以基本上我在只有域可以访问的域内创建读取端。我在添加新项目之前对其进行查询,以便如果它已经存在,我只使用已经存在的?是的,我想了一个晚上,看来我不仅要在创建新聚合之前进行这样的域存储和查询,还必须引入一些补偿动作。例如,如果同时发送多个创建同一个项目的请求,则会创建两个相同的项目。所以我需要我的域存储作为事件处理程序,如果用户创建了相同的项目 - 我需要触发补偿命令以使用现有的项目删除/移动/重新创建这个项目......
更新 2
我也在考虑为此目的创建另一个聚合 - 为我的项目唯一性的 范围 聚合(在这个特定场景中 - GlobalScopeAggregate 或 DomainAggregate),它将包含 {name, Guid}键值引用。单独的 GlobalScopeHandler 将负责 ProjectCreated、ProjectArchived、ProjectRenamed 事件,并且如果 ProjectCreated 事件以已创建的相同名称发生,最终将触发补偿操作。但我对补偿行动感到困惑。如果用户已经进行了备份并且在他的界面中对项目有相关视图,我应该如何反应?他可以更改错误项目的描述、名称等,这些项目已经通过补偿措施被删除。此外,我的补偿操作将删除 Project 和 备份聚合,并使用现有 ProjectId 创建新的备份聚合,因为我的备份聚合在 ProjectId 字段上没有设置器(它是备份执行操作的不可变记录) .这正常吗?
更新 3 - 域说明
在广泛的网络上有许多工业设备(BackupableThing、可编程控制器),其中有一些固件编程。客户更新固件并将其上传到控制器(可备份的东西)。这个程序被备份了。但是有很多相同类型的控制器,客户很可能会一遍又一遍地将相同的程序上传到多个控制器以及同一个控制器(作为逆转某些更改的一种手段)。用户需要反复备份所有这些控制器。备份是一些二进制数据(存储在控制器、程序中)和备份发生的日期。 Project 是封装二进制数据以及与备份相关的所有信息的实体。鉴于我无法在之前上传的状态下备份程序(我只能得到不可读的原始二进制数据,我也可以再次上传回控制器),我需要单独的聚合项目,它包含数据属性以及附加的数量文件(例如,固件项目文件)、描述、名称和其他字段。现在,每当备份某个控制器时,我不想显示“仅显示没有任何描述的二进制数据”并强制用户再次填写所有描述字段。我想查看是否已经使用相同的二进制数据完成了备份,然后只需将此项目链接到此备份,以便备份另一个控制器的用户将立即看到有关此控制器中的内容的大量信息。 :)
所以,我想这是基于集合的验证的情况,它经常发生(与常规的唯一约束相反),而且我会有很多备份,所以单独的聚合把这一切都留在记忆中是不明智的。
另外我只是认为还有另一个问题。我无法计算二进制数据的哈希值,并且容忍两个不同备份被视为同一个项目的小风险。这是需要精确和强大解决方案的行业领域。同时,我不能在二进制数据列(SQL 中的 varbinary)强制唯一约束,因为我的二进制数据可能比较大。所以我想我需要为[int(二进制数据的哈希),Guid(项目的id)]关系创建单独的表,如果找到新备份的二进制数据的哈希,我需要加载相关的聚合并确保二进制数据是一样的。如果不是 - 我还需要某种机制来存储具有相同哈希的多个关系。
当前实施
我最终创建了包含两列的单独表:DataHash (int) 和 AggregateId (Guid)。然后我创建了具有工厂方法 GetOrCreateProject(Guid id, byte[] data) 的 域服务。此方法通过计算的数据哈希获取聚合 id(如果有多个具有相同哈希的行,它将获取多个值),加载此聚合并比较数据参数和聚合.Data 属性。如果它们相等 - 返回现有和加载的聚合。如果它们不相等 - 将新的哈希实体添加到哈希表并创建新的聚合。
这个 hash 表现在是域的一部分,现在域的一部分不是事件来源的。未来对唯一性验证的所有需求(例如 BackupableThing 的名称)都意味着创建这样的表,这些表将基于状态的存储添加到域端。这增加了整体复杂性并紧密绑定域。这就是我开始思考事件溯源是否适用于此的点,如果不适用,它到底适用于哪里?我试图将其应用于简单的系统,以增加我的知识并充分理解 CQRS/ES 模式,但现在我正在与基于集合的验证的复杂性作斗争,并看到带有某种 ORM 的简单的基于状态的关系表会更好的情况(因为我什至不需要事件日志)。
【问题讨论】:
-
那么,如果
BackupableThing已经备份了相同的结果,唯一可以告诉你的就是计算结果吗?既然它不会提高性能,那么您从该唯一性规则中获得了什么?我的意思是,备份不像是需要唯一标识的业务对象... -
“我无法计算二进制数据的哈希值,并且容忍两个不同备份被视为同一个项目的小风险” - 为什么?会有什么后果?
-
@guillaume31 如果用户备份了一些控制器,他希望能够在一段时间内恢复给定的备份,并且控制器应该按预期工作。例如,在执行成功备份后,发现已经创建的项目具有相同的数据散列(但具有 另一个 实际数据)。不仅用户会看到错误的描述,而且如果他选择再次上传“相同的”程序来恢复备份 - 此操作将恢复错误的程序并且控制器开始工作错误。这些控制器控制着水泵站,因此可能导致灾难性后果。
-
这与 DDD 有什么关系?如果您的担忧是正确的,那么您有哈希函数熵问题,而不是 DDD 问题,对吗?我的意思是,如果您发现另一个项目与您当前正在执行的备份具有相同的哈希值,您如何知道它是否是相同的数据?
-
我会加载相关的聚合并检查。它比将 2-MByte 数据字段作为唯一键更好,这是一个实现细节。如果我会使用纯 DDD 并忘记性能问题,我会将我的 byte[] 字段设置为唯一键并仅在此 byte[] 字段没有时创建新项目。
标签: c# .net domain-driven-design cqrs event-sourcing