【问题标题】:How to query aggregate root by some other property apart from Id?如何通过 Id 之外的其他属性查询聚合根?
【发布时间】: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


【解决方案1】:

无需“查询读取端”——因为这是个坏主意。您所做的只是为域创建域存储模型。

因此,您将把域对象保存到 EventStore,并将一些特殊的东西保存在 SQL、Key-Value 等其他地方。然后读取消费者在 SQL 中构建您的读取模型。

例如,在我的应用程序中,我的域实例监听事件以构建域查询模型,我将其保存到 riak kv。

一个简单的例子应该能说明我的意思。查询是通过查询处理器处理的,这是一种流行的模式

class Handler : 
    IHandleMessages<Events.Added>,
    IHandleMessages<Events.Removed>,
    IHandleQueries<Queries.ObjectsByName>
{
    public void Handle(Events.Added e) {
       _orm.Add(new { ObjectId = e.ObjectId, Name = e.name });
    }
    public void Handle(Events.Removed e) {
       _orm.Remove(x => x.ObjectId == e.ObjectId && x.Name == e.Name);
    }
    public void Handle(Queries.ObjectsByName q) {
        _orm.Query(x => x.Name == q.Name);
    }

}

【讨论】:

  • 更新了我关于您的回答的问题,也是的,我使用查询/事件处理程序等模式。
  • 关于您的编辑 - 如果您需要知道项目是否存在,您最好检查项目流的存在并将项目 ID 设置为可以查询事件存储的字符串。然后你就会知道你在任何时候都只创建 1 个项目
  • “对象保存在 EventStore 中,一些特殊的东西保存在其他地方”——这意味着你在一个事务中有两个不同的基础设施,这意味着两阶段提交,这不是一个好主意。跨度>
  • @AlexeyZimarev 它不是 2 部分提交,命令被处理并且 1 个东西(单个流)被更新。读取命令产生的事件是一个不同的事务,写入另一个新事物。 2 笔交易 2 件事情保存
【解决方案2】:

我的回答很笼统,因为我不确定是否完全理解您的问题域,但解决集合验证问题只有两种主要方法。

1。强制执行强一致性

强制执行强一致性意味着不变量将受到事务性保护,因此永远不会被违反。

强制执行强一致性很可能会限制系统的可扩展性,但如果您负担得起,那么这可能是最简单的方法:防止冲突发生,而不是事后处理冲突通常更容易。

有很多方法可以强制执行强一致性,但这里有两种常见的方法:

  1. 依赖数据库唯一约束:如果您有一个支持它们的数据存储,并且您的事件存储和此数据存储可以参与同一个事务,那么您可以使用这种方法。

    例如(伪代码)

    transaction {
        uniquenessService.reserve(uniquenessKey); //writes to a DB unique index
    
        //save aggregate that holds uniquenessKey
    }
    
  2. 使用聚合根:这种方法与上述方法非常相似,但一个区别是规则显式地存在于域中而不是数据库中。聚合将负责维护内存中的唯一性键集。

    鉴于每次您需要记录一个新的键时都必须将整个键集放入内存中,您可能应该始终将这些类型的聚合缓存在内存中。

    我通常只在有非常小的一组潜在唯一键时才使用这种方法。它在唯一性规则本身非常复杂而不是简单的键查找的情况下也很有用。

请注意,即使在强制执行强一致性时,UI 也可能会阻止发送无效命令。因此,您还可以通过读取模型获得唯一性信息,UI 会使用这些信息来及早检测冲突。

2。最终一致性

在这里,您将允许违反规则,但随后执行一些补偿操作(自动或手动)来解决问题。

有时,强制执行强一致性会受到过度限制或具有挑战性。在这些情况下,您可以询问企业他们是否愿意在事后解决违反规则。重复通常非常罕见,特别是如果 UI 在发送命令之前确实验证了命令(黑客可能会滥用客户端检查,但这是另一回事)。

在解决一致性问题时,事件是很好的挂钩。您可以侦听诸如SomeThingThatShouldBeUniqueCreated 之类的事件,然后发出查询以检查是否存在重复。

将以企业希望的方式处理重复项。例如,您可以向管理员发送消息,以便他手动解决问题。


尽管我们可能认为始终需要强一致性,但在许多情况下并非如此。您必须与业务专家一起探讨在一段时间内允许违反规则的风险,并确定这种情况发生的频率。有时您可能会意识到,业务并没有真正的风险,并且强一致性是开发人员人为强加的。

【讨论】:

  • 但是在这里,唯一性约束只能在(可能成本高昂的)计算完成后检查。我想在用户点击和显示唯一性违规错误消息之间可能需要几分钟。聚合(或 DDD)仍然适合这种情况吗?
  • @guillaume31 也许不是。这就是为什么我选择描述各种方法而不是建议一种特定的方法。在上述情况下,为什么必须强制执行唯一性实际上是值得怀疑的。我不确定我是否了解该域?我的意思是,为什么备份某些东西会创建一个项目和备份?什么是项目?为什么要注意防止多次备份?
  • 我也在问自己同样的问题 ;) 肯定需要澄清一下。
  • @EwanCoder IMO,与目标(防止用户重新输入描述)相比,您当前的模型不仅过于复杂,而且还与“备份”的想法相矛盾,因为它通常不被理解。
  • @EwanCoder 我不确定 DDD 模式是否适合您的问题,如果适合,您会觉得您缺少一些重要的领域概念,这些概念会使建模变得直观。
【解决方案3】:

当领域的主要方面尚未得到充分分析或表达时,您过早地将问题硬塞进 DDD 模式中。这是一种危险的组合。

  • 什么是项目,如果您询问您所在领域的专家? (提示:可能不是"Project is some entity to encapsulate binary data"
  • 什么是备份,如果您询问您所在领域的专家
  • 在现实世界中应该满足哪些限制条件?
  • 关于备份的典型用例是什么?

当您向问题添加更新和 cmets 时,我们会逐步了解其中的一些内容,但这是错误的方法。

不要将聚合和存储库以及投影和唯一键作为起点。相反,首先要为您的领域术语写下清晰的定义。用户正在执行哪些业务流程?既然您说要使用事件溯源,那么发生了哪些事件?弄清楚你的领域是否足够丰富,让 DDD 成为一种相关的建模方法。当所有这些都清楚地说明时,您将有文字来描述您的备份唯一性问题并从更相关的角度处理它。我不认为你现在拥有它们。

【讨论】:

  • 项目也是一种基础设施术语。如果我选择纯 DDD,客户希望从控制器备份数据,例如,每天一次。并且每个备份都应该有 byte[] 数据(以便以后能够恢复它)和 Date 字段以检查何时执行。然后业务添加了一个要求,即能够将一些文件和描述附加到给定的备份中,以便用户稍后记住给定的备份是关于什么的。我个人认为不复制相同的备份会很酷:)
  • 为什么要让用户自己做备份?这些备份有什么用?似乎拥有一个只备份唯一控制器的自动备份解决方案会更容易。
  • 我个人认为不复制相同的备份会很酷 - 如果它不是一个明确的要求并且会导致您意外复杂,它可能不会真是个好主意。
猜你喜欢
  • 2018-10-15
  • 2022-12-10
  • 1970-01-01
  • 2014-01-15
  • 1970-01-01
  • 2023-04-10
  • 1970-01-01
  • 2014-04-13
  • 2011-01-16
相关资源
最近更新 更多