当使用Parent-Child-GrandChild ...关系的事件采购时,CQRS读取模型设计

时间:2014-03-20 03:51:15

标签: cqrs event-sourcing

我正在编写我的第一个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命令处理程序是否应该查询要添加到事件的团队信息?这可能还不存在,因为读取模型可能尚未与团队一起更新。

如果我想显示团队名称和已分配给团队的参与者,并在此特遣队视图中命名队长,该怎么办?我是否也将参与者存储在阅读模型中?这似乎有点重复。

对于一些额外的背景。参与者不一定是任何特遣队或团队的一部分,他们可能只是客人;或特遣队的代表和/或备件。但是,角色可以改变。非团队成员的代表也可能是一名备用人员,由于受伤将被分配给团队,但他们仍然是代表。这就是使用通用“参与者”的原因。

我理解我希望阅读模型不会比必要的重,但我很难理解我可能需要来自我没有订阅的事件(TeamCreated)的数据,理论上不应该,但由于未来的事件(TeamAssignedToContingent),我确实需要这些信息,我该怎么办?

更新:一夜之间想到这一点,似乎我想到的所有选项都很糟糕。必须有另一个。

选项1 :向已提升的事件添加更多数据

  • Do命令处理程序是否开始使用可能尚未写入的读取模型,以便获取该数据?
  • 如果活动的未来订阅者需要更多信息,该怎么办?我无法添加它!

选项2 :让事件处理程序订阅更多活动吗?

  • 这会导致读取模型存储可能不关心的数据?
  • 示例:将参加者存储在ContingentView阅读模型中,以便当某人被分配到团队并标记为队长时,我们就知道他们的名字。

选项3 :让事件处理程序查询其他读取模型吗?

  • 这感觉就像最好的方法,但感觉不对。如果需要,“条件”视图是否应查询“参与者”视图以获取名称?它确实消除了1和2中的draback。

选项4 :...?

2 个答案:

答案 0 :(得分:5)

您可能最终会得到的是1和2的组合。增强或丰富事件以获取更多信息绝对没有问题。这实际上非常有用。

  • 如果活动的未来订阅者需要更多信息,该怎么办?我无法添加它!

您可以将其添加到未来的活动中。如果您需要历史数据,您必须在其他地方找到它。您不能在世界上的所有数据上构建事件,只是因为您将来需要它。甚至允许返回并向您的历史事件添加更多数据。有些人会因为重写历史而对此不屑一顾,但你不是,你只是在改善历史。只要您不改变现有数据,您应该没问题。

我怀疑你的事件处理程序也需要随着时间的推移订阅更多事件。例如,团队是否需要重命名?如果是这样,那么您将需要处理该事件。不要害怕在不同的阅读视图中拥有相同数据的多个副本。来自关系数据库背景,这种重复数据是最难以使用的事情之一。

最终务实。如果您在应用CQRS时遇到痛苦,请更改您应用它的方式。

答案 1 :(得分:1)

我想到的另一个选择是:

在您的阅读方面,您首先要通过写入方面的事件构建规范化视图。

E.g:

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

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

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

ContingentCreated 事件中,您只需将记录插入特遣队表。

TeamCreated 事件中,您只需将记录插入团队表。

TeamAssignToContingent 事件中,您将记录插入 contigents_teams 表。

TeamNameChanged 事件中,您可以更新球队表中的相应记录。

等等。

因此,您的数据(最终)一致并与写入方同步。

是的,你需要在读取端使用连接来获取数据......

如果此规范化视图不满足读取性能需求,则可以从此规范化数据构建非规范化视图。 它需要一个额外的步骤来构建非规范化视图,但对我来说,这是我现在能想到的最简单的解决方案。

也许你甚至不需要非规范化的观点。

毕竟(在我看来),ES的主要价值在于捕获用户意图,对数据的信心(没有破坏性操作,单一事实来源),能够回答过去没人能想到的问题,大分析的价值。

正如我所说,如果你需要优化性能,你总是可以从读取端的规范化视图构建非规范化视图。