【问题标题】:Event Sourcing and Read Model generation事件溯源和读取模型生成
【发布时间】:2011-05-03 02:52:30
【问题描述】:

假设堆栈溢出域问题和事件定义如下:

UserRegistered(UserId, Name, Email)
UserNameChanged(UserId, Name)
QuestionAsked(UserId, QuestionId, Title, Question)

假设事件存储的状态如下(按出现顺序):

1) UserRegistered(1, "John", "john@gmail.com")
2) UserNameChanged(1, "SuperJohn")
3) UserNameChanged(1, "John007")
4) QuestionAsked(1, 1, "Help!", "Please!")

假设问题列表的以下非规范化阅读模型(对于 SO 的第一页):

QuestionItem(UserId, QuestionId, QuestionTitle, Question, UserName)

以及以下事件处理程序(构建非规范化读取模型):

public class QuestionEventsHandler
{
    public void Handle(QuestionAsked question)
    {
        var item = new QuestionItem(
            question.UserId, 
            question.QuestionId, 
            question.Title, 
            question.Question, 
            ??? /* how should i get name of the user? */);
        ...
    }
}

我的问题是我怎样才能找到提出问题的用户的姓名?或者更常见:如果我的非规范化读取模型需要特定事件中不存在的额外数据,我应该如何处理事件?

我检查了现有的 CQRS 样本,包括 Greg Young 的 SimpleSQRS 和 Mark Nijhof 的 Fohjin 样本。但在我看来,它们只对事件中包含的数据进行操作。

【问题讨论】:

标签: domain-driven-design cqrs event-sourcing


【解决方案1】:

我个人认为从事件处理程序中查找用户名没有任何问题。但是,如果您无法从用户的读取模型中查询名称,那么我会向 QuestionEventsHandler 引入一个额外的事件处理程序,以处理 UserRegistered 事件。

这样,QuestionEventsHandler 可以维护自己的用户名存储库(您不需要存储用户电子邮件)。 QuestionAsked 处理程序可以直接从它自己的存储库中查询用户的姓名(正如 Rinat Abdullin 所说的存储很便宜!)。

此外,由于您的 QuestionItem 读取模型包含用户名,您还需要在 QuestionEventsHandler 中处理 UserNameChanged 事件,以确保 QuestionItem 中的 name 字段是最新的。

在我看来,这似乎比“丰富事件”要省力,而且好处是在系统的其他部分及其读取模型上建立依赖关系。

【讨论】:

  • 如果你不小心,你最终可能会在你的读取模型中到处都有大量重复数据
  • 正确,但读取模型通常是非规范化的,因此总是会有重复数据。
  • 我猜,问题相关事件和用户相关事件在不同的主题/流中调度。因此,例如当用户首先更改他的用户名时,会发布 UsernameChanged 事件,并且在他创建问题并发布 QuestionCreatedEvent 之后,不保证会首先处理 UsernameChanged 事件。所以 QuestionCreatedEvent 的处理程序可能会从其本地存储的用户名中读取旧用户名,所以我们会得到错误的读取端
  • @Teimuraz 我认为你的意思是 UserNameChanged 可以在 QuestionedAsked 之后处理;我认为 QuestionEventsHandler 是否维护自己的名称存储库并没有什么不同。事件顺序有 2 种情况:1)改名后问问题或 2)问问题后改名;在这两种情况下,如果您关心问题上的名称已过时,那么 QuestionEventsHandler 将需要处理名称更改事件。例如,考虑在 1 周或 1 个月或一年后的事件之后提出问题然后更改名称...
  • 是的,你是对的。我们可以使用两个方向:在处理 QuestionAskedEvent 时,我们从本地用户名存储中获取用户名(在 UsernameChangedEvent 上更新)。当我们在本地用户名存储中处理 UsernameChanged 事件时,如果需要,我们还可以在问题投影中更新用户名。
【解决方案2】:

从 EventStore 中拉取事件。

记住 - 您的读取模型需要已经对 EventStore 具有只读访问权限。阅读模型是一次性的。它们只是缓存视图。您应该能够随时删除/过期您的读取模型 - 并从 EventStore 自动重建您的读取模型。所以 - 您的 ReadModelBuilders 必须已经能够查询过去的事件。

public class QuestionEventsHandler
{
    public void Handle(QuestionAsked question)
    {
        // Get Name of User
        var nameChangedEvent = eventRepository.GetLastEventByAggregateId<UserNameChanged>(question.UserId);

        var item = new QuestionItem(
            question.UserId, 
            question.QuestionId, 
            question.Title, 
            question.Question, 

            nameChangedEvent.Name
    }
}

还要意识到 - EventStore 存储库不一定是真正的 EventStore,尽管它肯定可以。分布式系统的优势在于,如果需要,您可以轻松地复制 EventStore,使其更接近您的 ReadModel。

我遇到了完全相同的情况...我需要比单个事件中可用的更多数据。对于需要使用初始状态填充新 ReadModel 的 Create 类型事件尤其如此。

从读取模型:您可以从其他读取模型中提取。但我真的不推荐这样做,因为你会引入一大堆依赖泥,其中视图依赖视图依赖视图。

事件中的附加数据:您真的不想用视图所需的所有额外数据使事件变得臃肿。当您的域发生更改并且您需要迁移事件时,它真的会伤害您。领域事件有特定的用途——它们代表状态变化。不查看数据。

希望这会有所帮助 -

瑞恩

【讨论】:

  • 这假设您可以访问事件存储库,在我看来,处理程序应该只接收事件而不是拉事件。这可以在高度分布式的系统中工作吗?
  • "您的读取模型需要对 EventStore 具有只读访问权限"。这对我来说是个新闻。我看不出为什么读取端需要访问事件存储。它只需要在事件发生时发出警报。我个人对从读取端查询用户名没有问题,因为无论如何处理程序都存在于读取端。
  • 在我们当前的项目中,我们很可能拥有对事件存储的读取权限,因为与特定聚合相关的事件流将显示在 UI 中,facebook 时间线样式。是的,我们可以使用投影并生成它(如果这需要一些非规范化,也许我们会这样做),但现在我们正在寻找最直接的查询方式。
【解决方案3】:

用所有必要的信息丰富活动。

我记得 Greg 的方法 - 在以这种方式创建和存储/发布活动的同时丰富活动。

【讨论】:

  • 谢谢瑞纳特!我会接受您的建议,但您是否真的同意这是使用仅读取模型所需的数据来丰富领域事件的好解决方案?
  • 是的。我看不出有什么大的缺点。此外,现在存储很便宜,丰富的域事件也有助于以后分析您的系统。例如,我经常在对 IO 或 CPU 有压力的操作中放入很多性能统计信息;此信息甚至不用于读取模型。但我需要优化性能,我可以使用 LINQ 查询域日志以获取操作历史记录和确切的性能详细信息。
  • 这个答案表明所有需要的数据都应该在事件中。作为经验法则,我不同意这一点。事件处理程序将创建非规范化记录,通常包含不一定来自单个聚合的聚合和计算字段。也许以“NumberOfQuestionsByMonth”视图模型为例。这就是 CQRS 的重点;当某些东西被持久化而不是被查询时,它会进行这些计算。要进行这些计算,您通常需要查询和处理数据。此数据超出聚合范围,无法在事件中传递。
  • 这似乎是一个混乱的解决方案。如果你想加入两个聚合,然后监听来自两者的事件,或者投射到一个可以为你做连接的读取模型中。
  • 我也不同意,因为一个缺点可能是您需要更改过去发生的所有事件以包含其他数据(考虑到您从一开始就不知道的情况以后可能需要哪些数据)。您肯定不想包含未来可能感兴趣的所有内容。
猜你喜欢
  • 2018-04-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-12-28
  • 1970-01-01
  • 2020-05-27
  • 2020-02-21
相关资源
最近更新 更多