当域事件影响同一有界上下文中的多个聚合时,EventSourcing中的StreamId是什么?

问题描述 投票:1回答:2

Streams

一些作者建议在“流”中对事件进行分类,许多作者用“聚合ID”识别“流”。

说一个事件car.repainted,我们的意思是我们用id 12345重新粉刷到{color:red}

在这个例子中,流Id可能类似于car.12345,或者如果你有通用唯一ID,那么只需要12345

实际上,有些作者建议将事件流存储到一个表格中,该表格的结构或多或少类似于以下内容(如果你选择关系):

| writeIndex | event | cachedEventId | cachedTimeStamp | cachedType    | cachedStreamId |
| 1          | JSON  | abcd          | xxxx            | car.repainted | 12345          |
  • event列具有事件的“原始”值对象,如果它是关系数据库,则很可能序列化为JSON。
  • writeIndex仅用于数据库管理,与域本身无关。您可以将事件“转储”到另一个数据库中,并重写writeIndex,没有副作用。
  • cached*字段用于轻松查找和过滤事件,它们都可以从事件本身计算。
  • 特别值得一提的是cachedStreamId将被用于 - 根据一些作者 - 被映射到“事件所属的聚合Id”。在这种情况下,“汽车由12345确定”。

如果你不使用关系,你可能会将你的事件“作为文档”存储在数据湖/事件存储/文档仓库中,或者调用它你想要的方式(mongo,redis) ,elasticsearch ...)然后你做桶或组或选择或过滤器按标准检索一些事件(其中一个标准是“我感兴趣的实体/聚合ID => streamId”)。

Replaying

当重放事件以创建新的投影时,你只有一堆订阅者参与事件类型(可能还有版本),如果它适合你,你会阅读事件的完整原始文档,进行处理,计算和更新投影。如果事件不适合你,你就跳过它。

在重放时,将要重建的聚合读取表还原到已知的初始集(可能是“全空”),然后选择一个或多个流,按时间顺序选择事件并迭代更新聚合的状态。

Okey...

这一切对我来说都是合理的。直到这里才有消息。

Question

但是......我现在脑子里有一些短路......这是一个如此基本的短路,可能答案是如此明显,以至于我现在无法看到它会感到愚蠢......

如果一个事件对两个不同类型的聚合“同样重要”(假设它们位于相同的有界上下文中),或者即使它指的是同一聚合类型的两个实例,也会发生什么。

Example of 2 equally-important different aggregates:

想象一下,你在火车行业,你有这些聚合:

Locomotive
Wagon

想象一下,一辆机车可以搭载0或1辆货车,但货车不多。

你有这些命令:

Attach( locomotiveId, wagonId )
Detach( locomotiveId, wagonId )

如果机车和货车已经连接到某物上,则可以拒绝附加,如果在没有附加命令时发出命令,则可以拒绝分离。

这些事件显然是相应的:

AttachedEvent( locomotiveId, wagonId )
DetachedEvent( locomotiveId, wagonId )

问:

那里的流ID是什么?火车头和旅行车都同等重要,它不是“火车头”或“旅行车”的事件。这是一个影响这两个领域的事件! streamId是哪一个?为什么?

Example with 2 aggregates of the same type

说问题跟踪器。你有这个聚合:

Issue

而这些命令:

MarkAsRelated( issueAId, issueBId )
UnmarkAsRelated( issueAId, issueBId )

如果商标已经存在并且商标被拒绝,则商标被拒绝,之前没有任何商标。

那些事件:

MarkedAsRelatedEvent( issueAId, issueBId )
UnmarkedAsRelatedEvent( issueAId, issueBId )

问:

同样的问题:这不是关系“属于”问题A或B.它们是否相关。但它的双向性。如果A与B相关,则B与A相关。这里的streamId是什么?为什么?

History is written once

无论如何,我没有看到为每个事件创建两个事件。这是计算器的问题......

如果我们看到“历史”的定义(一般不在计算机中!),它会说“发生了一系列事件”。在自由词典中,它说:“事件的时间顺序记录”(https://www.thefreedictionary.com/history

因此,当社交群体A和社交群体B之间发生战争并且说B击败A时,你不会写2个事件:lost(A)won(B)。你只写一个事件warFinished( wonBy:B, lostBy:A )

Question

那么当事件影响当时的多个实体时,你如何处理事件流呢?并不是它“属于”一个而另一个是对它的补充,但它真的等于两者?

stream domain-driven-design cqrs event-sourcing aggregateroot
2个回答
1
投票

会发生什么...如果事件对两个不同类型的聚合“同等重要”(假设它们在同一个有界上下文中)或者甚至它引用了相同聚合类型的两个实例

是一个简单的(注意:不容易)的想法。我们将聚合保存到稳定存储时,而不是覆盖以前的状态,而是编写一个新版本,链接回以前的版本。此外,我们不是写出新版本的整个副本,而是写出差异,并且差异以特定于域的方式表达。

因此,将聚合保存到流类似于将聚合的表示形式保存为键值存储中的文档,或者保存为关系数据库中的行。

当您询问它属于哪个“流”时:它属于更改的聚合流,就像在其他任何存储策略中一样。

如果您不确定哪个聚合发生了变化,那么您所拥有的是建模问题,而不是事件来源问题。

您的两个示例都描述了在两个聚合之间引入关系;它类似于在数据库中的两个表之间建立多对多的关系。谁拥有M2M表?

好吧,如果聚合都不需要该信息来确保其自身的不变性,那么M2M表可能本身就是一个聚合。

想象一下两方之间的合同代表 - 可能会发现双方是偶然的,而“合同”是一个重要的想法,值得建模为自己的事物。

如果关系明显是“一部分”的一部分(该聚合是保护依赖于关系状态的不变量),那么该聚合将负责编辑新表,而另一个聚合将忽略它。

如果两个聚合都关心关系,那么你有两个问题之一

1)您对域名的分析是错误的 - 您在错误的地方绘制了聚合边界。把你带到白板并开始画出来。

2)您有两个关系副本 - 每个聚合一个副本,但这些副本不一定相互一致。

这是一个重要的启发式方法:如果你真的有两个不同的聚合,你应该能够将它们存储在两个完全不同的数据库中。他们无法共享彼此的数据,但他们可以保留自己的其他人数据的版本/时间戳/缓存副本。

因此,左手聚合进行了更改,“管道”将“左手聚合更改”消息发送到右手聚合,然后右手聚合更新其缓存。

请注意,在我们认为合同是管理其自身状态的头等问题的情况下,这将如何工作。模型更新合同,将更改保存到其状态,然后管道出现并将更改的副本传递给左手聚合和右手聚合。

Simple。不一定容易。


0
投票

我不认为它与事件采购本身有任何关系。也许设计可以修改一下。

我会为这个机车选择这样的东西:

public class Locomotive
{
    Guid Id { get; private set; }
    Guid? AttachedWagonId { get; private set; }

    public WagonAttached Attach(Guid wagonId)
    {
        return On(
            new WagonAttached
            {
                Id = wagonId
            });
    }

    private WagonAttached On(WagonAttached wagonAttached)
    {
        AttachedWagonId = wagonAttached.Id;

        return wagonAttached;
    }
}

Locomotive的事件流是WagonAttached事件所在的位置。以什么方式Wagon聚合依赖于这个事件是有争议的。我认为旅行车可能并不太在意,因为Product并不太关心Order(在这种情况下可能与之相关)。汇总Order似乎更适合OrderItem联想实体。我猜你的机车到旅行车的关系可能会遵循相同的模式,因为机车会连接一辆以上的货车。可能对设计有点多,但我会假设这些是假设的例子。

Issue也是如此。如果一个人可以附加多个,那么OrderProduct概念就会发挥作用。即使涉及两个问题,也有一个方向,因为作为下属的一个问题与主要问题相关。也许有RelationshipType的事件,例如DependencyImpediment等。在这种情况下,可能会使用值对象来表示:

public class Issue
{
    public class RelatedIssue
    {
        public enum RelationshipType
        {
            Dependency = 0,
            Impediment = 1
        }

        public Guid Id { get; private set; }
        public RelationshipType Type { get; private set; }

        public RelatedIssue(Guid id, RelationshipType type)
        {
            Id = id;
            Type = type;
        }
    }

    private readonly List<RelatedIssue> _relatedIssues = new List<RelatedIssue>();

    public Guid Id { get; private set; }

    public IEnumerable<RelatedIssue> GetRelatedIssues()
    {
        return new ReadOnlyCollection<RelatedIssue>(_relatedIssues);
    }

    public IssueRelated Relate(Guid id, RelationshipType type)
    {
        // probably an invariant to check for existence of related issue

        return On(
            new IssueRelated
            {
                Id = id,
                Type = (int)type
            });
    }

    private IssueRelated On(IssueRelated issueRelated)
    {
        _relatedIssues.Add(
            new RelatedIssue(
                issueRelated.Id, 
                (RelatedIssue.RelationshipType)issueRelated.Type));

        return issueRelated;
    }
}

关键是事件属于单个聚合但仍然代表关系。你只需要确定最有意义的一面。

事件可以(或应该)使用一些事件驱动的体系结构方法(比如服务总线)发布,以便通知其他感兴趣的各方。

© www.soinside.com 2019 - 2024. All rights reserved.