Domain driven design 事件源和读取模型生成

Domain driven design 事件源和读取模型生成,domain-driven-design,cqrs,event-sourcing,Domain Driven Design,Cqrs,Event Sourcing,假设堆栈溢出域问题和以下事件定义: UserRegistered(UserId, Name, Email) UserNameChanged(UserId, Name) QuestionAsked(UserId, QuestionId, Title, Question) 假设事件存储的以下状态(按出现顺序): 假设问题列表采用以下非规范化阅读模型(SO第一页): 以及以下事件处理程序(用于构建非规范化读取模型): 我的问题是如何找到提出问题的用户的姓名?或者更常见:如果我的非规范化读取模型需要特

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

UserRegistered(UserId, Name, Email)
UserNameChanged(UserId, Name)
QuestionAsked(UserId, QuestionId, Title, Question)
假设事件存储的以下状态(按出现顺序):

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

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

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


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

只需使用所有必要的信息丰富事件即可


我记得,Greg的方法是在创建和存储/发布事件的同时丰富事件。

从EventStore中提取事件


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

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
    }
}
public class QuestionEventsHandler
{
公共无效句柄(问题)
{
//获取用户的名称
var nameChangedEvent=eventRepository.GetLastEventByAggregateId(question.UserId);
变量项=新问题项(
问题.UserId,
问题,问题ID,
问题.标题:,
问题,问题,,
nameChangedEvent.Name
}
}
还要认识到-EventStore存储库不必是真正的EventStore,尽管它当然可以。分布式系统的优点是,如果需要,您可以轻松地将EventStore复制到更接近ReadModels的位置

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

从读取模型:您可以从其他读取模型中提取。但我真的不建议这样做,因为您将引入一个大的依赖性泥球,其中视图依赖于视图依赖于视图

事件中的其他数据:您真的不想用视图所需的所有额外数据来填充事件。当域更改时,这会对您造成很大的伤害&您需要迁移事件。域事件有特定的用途-它们表示状态更改,而不是查看数据

希望这有帮助-


Ryan

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

通过这种方式,QuestionEventsHandler可以维护自己的用户名存储库(您不需要存储用户的电子邮件)。然后QuestionAsquired处理程序可以直接从自己的存储库中查询用户名(正如Rinat Abdullin所说,存储很便宜!)

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


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

谢谢Rinat!我同意你的建议,但你真的同意这是一个用仅用于读取模型的数据丰富域事件的好解决方案吗?是的。我看不出有什么大的缺点。此外,现在的存储很便宜,丰富的域事件也有助于以后分析您的系统。例如,我经常在强调IO或CPU的操作中放置大量性能统计数据;这些信息甚至不用于读取模型。但我需要优化性能,我可以用它们查询域日志LINQ提供操作历史记录和准确的性能详细信息。这个答案表明所有需要的数据都应该在事件中。我不同意这一点,因为经验法则。事件处理程序将创建一个非规范化记录,通常包含聚合和计算字段,这些字段不一定来自单个聚合。可能是“Numb”以erOfQuestionsByMonth的视图模型为例。这是CQR的观点;它在持久化某些内容时而不是在查询时执行这些计算。要执行这些计算,通常需要查询和处理数据。这些数据超出聚合范围,无法在事件中传递。这似乎是一个混乱的解决方案。I如果您想连接两个聚合,那么请同时侦听这两个聚合中的事件,或者将其投影到一个可以为您执行连接的读取模型中。我也不同意,因为一个缺点可能是您需要更改过去发生的所有事件,以同时包含附加数据(假设您从一开始就不知道以后可能需要哪些数据)。您肯定不希望包含将来可能感兴趣的所有内容。更多讨论请参见:假设您可以访问事件存储库,我认为处理程序应该只接收事件,而不是拉取事件。这在高度分布式系统中是否可行?“您的读取模型需要以只读方式访问事件存储”。这对我来说是新闻。我看不出读取端需要访问事件存储的原因。它只需要在事件发生时发出警报。我个人在从读取端查询用户名时没有问题,因为处理程序存在于
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? */);
        ...
    }
}
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
    }
}