当使用事件溯源和父子孙关系时,CQRS读模型的设计

11

我正在编写我的第一个CQRS应用程序,假设我的系统派发以下命令:

  • CreateContingent (Id, Name)
  • CreateTeam (Id, Name)
  • AssignTeamToContingent (TeamId, ContingentId)
  • CreateParticipant (Id, Name)
  • AssignParticipantToTeam (ParticipantId, TeamId)

目前,这些命令会产生相同的事件,只是用过去式表述(例如ContingentCreated,TeamCreated等),但它们包含相同的属性。(我不确定这是否正确,这是我的疑问之一)

我的问题在于读模型。

我有一个Contingents读模型,订阅了ContingentCreated事件,并具有以下方法:

    List<ContingentQueries.Contingent> GetContingents();
    ContingentQueries.Contingent GetContingent(System.Guid id);
    ContingentQueries.Contingent GetContingent(string province);

通过这些方法返回的 Contingent 类的样式如下:

    public class Contingent
    {
        public Guid Id { get; internal set; }
        public string Province { get; internal set; }
    }

这对于列表工作得很好,但是当我开始考虑用于查看单个队伍及其所有成员的视图的读模型时,情况开始变得混乱起来。鉴于这些方法,似乎微不足道:

    ContingentQueries.Contingent GetContingent(System.Guid id);
    ContingentQueries.Contingent GetContingent(string province);

我为这些查询创建了一个团队集合的Contingent类:

    public class Contingent
    {
        public Guid Id { get; internal set; }
        public string Province { get; internal set; }
        public IList<Team> Teams { get; internal set; }
    }

    public class Team
    {
        public Guid Id { get; internal set; }
        public string Name { get; internal set; }
    }
理论上,我只需要订阅ContingentCreated和TeamAssignedToContingent事件,但是TeamAssignedToContingent事件只有团队和协调员id,所以我无法设置此读取模型的Team.Name属性。
我是否还应该订阅TeamCreated?添加另一个团队副本供此处使用吗?
或者当引发事件时,它们是否应该具有更多信息? AddTeamToContingent命令处理程序是否应查询团队信息以添加到事件中?这可能尚不存在,因为读取模型可能尚未更新团队。
如果我想在这个Contingent视图中显示团队名称和已分配给团队并命名为队长的参与者怎么办?我也要在阅读模型中存储参与者吗?这似乎是大量重复的工作。
对于一些额外的背景信息。参与者不一定是任何协调员或团队的一部分,他们可能只是来宾;或协调员的代表和/或备用。然而,角色可能会转移。一个不是团队成员的代表也可以是替补,并由于受伤被分配到一个团队,但他们仍然是代表。这就是为什么使用通用的“参与者”的原因。
我明白我希望读取模型不要比必要的重,但我很难处理这样一个事实,即我可能需要来自我没有订阅的事件(TeamCreated)的数据,理论上不应该,但由于未来的事件(TeamAssignedToContingent),我确实需要那些信息,我该怎么办?
更新:经过一夜的思考,似乎我想到的所有选项都不好。必须还有其他方法。
选项1:向引发的事件添加更多数据
命令处理程序是否开始使用尚未编写的读取模型,以便获取数据?
如果未来的订户需要更多信息怎么办?我不能添加它!
选项2:使事件处理程序订阅更多事件?
这导致读取模型存储它可能不关心的数据吗?
例如:在ContingentView读取模型中存储参与者,以便当一个人被分配到一个团队并标记为船长时,我们知道他们的名字。
选项3:使事件处理程序查询其他读取模型?
这感觉是最好的方法,但感觉不对。协调员视图是否应查询参与者视图以获取名称(如果需要)?它消除了1和2的缺点。
选项4:...?
2个回答

6
你最终可能会得到1和2的结合体。增强或丰富事件以获得更多信息是完全没有问题的,这实际上非常有帮助。
如果未来的订阅者需要更多信息怎么办?我不能添加它!
你可以将其添加到未来的事件中。如果你需要历史数据,你需要在其他地方找到它。你不能为了偶然需要它而在事件中构建所有世界上的数据。甚至允许回到历史事件中添加更多数据。有些人会对此表示反感,认为这是篡改历史,但实际上你只是增强了历史。只要不改变现有数据,你就应该没问题。
我猜你的事件处理程序也会随着时间的推移需要订阅更多的事件。例如,团队是否需要重命名?如果是,那么你将需要处理该事件。不要害怕在不同的读取视图中拥有相同数据的多个副本。从关系数据库背景出发,这种数据复制是最难适应的事情之一。
最后务实一点。如果应用CQRS时遇到了困难,那么就改变应用方式。

2

我想到的另一个选项是:

在您的读取端,首先通过来自写入端的事件构建规范化视图。

例如:

contingents table:
-----------
id, name

teams table:
-----------
id, name

contigents_teams table:
-----------
contigent_id, team_id

ContingentCreated事件中,您只需将记录插入contingents表中。
在事件中,您只需将记录插入teams表中。
在事件中,您将记录插入contigents_teams表中。
在事件中,您需要更新teams表中的相应记录。
因此,您的数据(最终)与写入侧保持一致并同步。
是的,您需要在读取侧使用连接来获取数据...
如果这种规范化视图不能满足读取性能需求,则可以从这个规范化数据构建一个非规范化视图。它需要额外的步骤来构建非规范化视图,但对我来说,这是目前我能想到的最简单的解决方案。
也许您甚至不需要一个非规范化视图。
毕竟(在我看来),ES的主要价值在于捕获用户意图,对数据的信心(没有破坏性操作,真相唯一),回答以前没有人能想到的问题,对分析具有很大的价值。
正如我所说,如果您需要优化性能,您总是可以从读取侧的规范化视图构建一个非规范化视图。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接