gpt4 book ai didi

domain-driven-design - 事件溯源 : proper way of rolling back aggregate state

转载 作者:行者123 更新时间:2023-12-04 04:24:07 35 4
gpt4 key购买 nike

我正在寻找与在 CQRS/事件源应用程序中实现回滚功能的正确方法相关的建议。
此应用程序允许一组编辑编辑和更新一些编辑内容,例如编辑新闻。我们实现了用户界面,以便每个字段都具有自动保存功能,现在我们希望为我们的用户提供撤消他们所做操作的可能性,以便可以将编辑新闻回滚到以前的已知状态。
基本上,我们希望实现类似于 Microsoft Word 和类似文本编辑器中的撤消命令的功能。在后端,社论新闻是我们域中定义的聚合实例,名为 故事 .
我们已经讨论了一些实现回滚的想法,我们正在寻找基于类似项目中真实世界经验的建议。以下是我们对此功能的考虑。
回滚如何在现实世界的业务领域中工作
首先,我们都知道在现实世界的业务领域中,我们所说的回滚是通过某种形式的补偿事件获得的。
想象一个与某种服务相关的域,可以为其购买订阅:我们可以有一个代表用户订阅的聚合和一个描述费用已与聚合实例相关联的事件(一个用户的特定订阅)。客户)。该事件的可能实现如下:

public class ChargeAssociatedToSubscriptionEvent: DomainEvent
{
public Guid SubscriptionId {get; set;}
public decimal Amount {get; set;}
public string Description {get; set;}
public DateTime DueDate {get; set;}
}
如果费用与订阅错误关联,则可以通过与同一订阅关联且金额相同的认证来修复错误,从而完全平衡费用的影响并让用户收回其款项.换句话说,我们可以定义以下补偿事件:
public class AccreditationAssociatedToSubscription: DomainEvent
{
public Guid SubscriptionId {get; set;}
public decimal Amount {get; set;}
public string Description {get; set;}
public DateTime AccreditationDate {get; set;}
}
因此,如果用户被错误地收取了 50 美元的费用,我们可以通过向用户订阅认证 50 美元来补偿错误:这样聚合的状态已经回滚到之前的状态。
为什么事情并不像看起来那么容易
基于前面的讨论,回滚似乎很容易实现。如果您在聚合修订版 B 处有故事聚合的实例,并且想要将其回滚到先前的聚合修订版,例如 A(A < B),则只需执行以下步骤:
  • 检查事件存储并获取修订版 A 和 B 之间的所有事件
  • 计算每个发生事件的补偿事件
  • 以相反的顺序将补偿事件应用于聚合

  • 不幸的是, 上一个过程的第二步并不总是可行的 :给定一个通用领域事件,并不总是可以计算其补偿事件,因为事件中包含的信息量不足以做到这一点。也许可以明智地定义所有事件,以便它们包含足够的信息来计算相应的补偿事件,但是在我们应用程序的当前状态下,有几个事件无法计算补偿事件,我们将宁愿避免改变我们事件的形状。
    基于状态比较的可能解决方案
    克服补偿事件问题的第一个想法是 通过将聚合的当前状态与目标状态进行比较来计算回滚聚合所需的最小事件集 .算法基本如下:
  • 获取当前状态下的聚合实例(称为 B)
  • 通过仅应用保留在事件存储中的前 n 个事件(我们的存储库允许通过指定聚合 id 和实现聚合的所需时间点来实现),获取目标状态下的聚合实例(称为 A)
  • 比较两个实例并计算要应用于状态 B 中的聚合的最小事件集,以便将其状态更改为 A
  • 将计算的事件应用于聚合

  • 基于事件重放的更智能方法
    解决回滚到聚合先前状态问题的另一种方法可能是 当聚合在特定时间点实现时,聚合存储库所做的事情与聚合存储库所做的相同 .为了做到这一点,我们应该定义一个事件,比如 StoryResettedEvent,它的作用是通过完全清空聚合来重置聚合的状态,并执行以下步骤:
  • 将 StoryResettedEvent 应用于我们的聚合,以便其状态被清空
  • 获取我们正在处理的聚合的前 n 个事件(从第一个保存的事件到目标状态 A 的所有事件)
  • 将所有事件应用到聚合实例

  • 我看到这种方法的主要问题是清空聚合状态的事件:这似乎有点人为,不是具有业务意义的真正领域事件,而是实现回滚功能的技巧。
    第三种方式:每次在事件存储中保存事件时,都将补偿事件持久化
    我们找到的获得我们需要的第三种方法再次基于补偿事件的概念。基本思想是 应用程序的每个事件都可以通过包含相应补偿事件的属性来丰富 .
    在引发事件的代码点,可以立即计算要引发的事件的补偿事件(基于聚合的当前状态和事件的形状),从而可以丰富事件有了这个信息,这种方式将被保存在事件存储中。通过这样做,补偿事件事件总是可用的,准备在回滚请求的情况下使用。该解决方案的缺点是必须修改每个域事件,并且只有我们必须计算并保存在事件存储中的最小部分补偿事件对实际回滚有用(其中大部分将永远不会被使用)。
    结论
    在我看来,解决问题的最佳选择是使用基于状态比较的算法(第一个提出的解决方案),但我们仍在评估要做什么。
    有没有人已经有类似的要求?还有其他方法可以实现回滚吗?我们是否完全忽略了这一点并遵循了错误的方法来解决问题?
    感谢您的帮助,任何建议将不胜感激。

    最佳答案

    如何生成补偿事件应该是 Story 聚合的关注点(毕竟,这是事件源中聚合的重点——它只是特定流的命令验证器和事件生成器)。

    据推测,您正在遵循典型的 CQRS/ES 流程:

  • 客户端发送一个撤消命令,大概说明它想要撤消回哪个版本,以及它的目标是什么故事
  • 撤消命令处理程序以通常的方式加载 Story 聚合,可能来自快照和/或通过将聚合的事件应用于聚合。
  • 以某种方式,命令被传递给聚合(可能是从命令中提取参数的方法调用,或者只是将命令直接传递给聚合)
  • 假设撤消命令有效,聚合以某种方式“返回”要持久化的事件。这些是补偿事件。

    • compute the compensation event for each of the occurred events

    ...

    Unfortunately, the second step of the previous procedure is not always possible



    为什么不?聚合已经传递了所有以前的事件,那么它需要什么它没有呢?聚合不仅会看到您想要回滚的事件,它还必须处理该聚合的所有事件。

    您真的有两个选择 - 通过让命令处理程序以某种方式提供帮助来减少聚合需要做的簿记,或者整个过程由聚合内部管理。

    命令处理程序提供帮助 :
    除了创建当前聚合之外,命令处理程序从命令中提取用户想要回滚到的版本,然后重新创建该版本的聚合(以通常的方式应用事件)。然后将旧聚合与命令一起传递给聚合的 undo 方法,以便聚合可以更轻松地进行状态比较。

    您可能认为这有点 hacky,但它似乎无害,并且可以显着简化聚合代码。

    聚合是独立的 :
    当事件应用于聚合时,它会将它需要的任何簿记添加到其状态,以便在它接收到撤消命令时能够计算补偿事件。这可以是补偿事件的映射,预先计算,可以恢复到的每个先前状态的列表(以允许状态比较),聚合已处理的事件列表(因此它可以在undo 方法),或者它需要的任何东西,它只是将它存储在它的内存状态(和快照状态,如果适用)。

    聚合自行完成的主要问题是性能 - 如果簿记状态的大小很大,则允许命令处理程序传递先前状态的简化将是值得的。在任何情况下,您都应该能够在 future 的任何时间在这些方法之间切换而不会出现任何问题(除非可能需要重建您的快照,如果您拥有它们)。

    关于domain-driven-design - 事件溯源 : proper way of rolling back aggregate state,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48531869/

    35 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com