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

Xav*_*ero 5 domain-driven-design stream aggregateroot cqrs event-sourcing

一些作者建议将事件分类为“流”,许多作者将“流”标识为“聚合 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          |
Run Code Online (Sandbox Code Playgroud)
  • event列具有事件的“原始”值对象,如果它是关系数据库,则很可能序列化为 JSON。
  • writeIndex仅仅是数据库管理,并没有任何与域本身。您可以将您的事件“转储”到另一个数据库中,并在没有副作用的情况下重写 writeIndex。
  • 这些cached*字段用于轻松查找和过滤事件,它们都可以从事件本身计算出来。
  • 特别值得一提的cachedStreamId是,根据一些作者的说法,将用于映射到“事件所属的聚合 Id”。在这种情况下,“汽车标识为12345”。

如果您不使用关系,您可能会将您的事件“作为文档”存储在数据湖 / 事件存储 / 文档仓库 / or-call-it-how-you-want (mongo, redis ,elasticsearch...),然后您创建存储桶或组或选择或过滤器以按条件检索某些事件(其中一个条件是“我感兴趣的实体/聚合 Id”=> 再次 streamId)。

重播

当重播事件以创建新的预测时,您只有一堆事件类型(可能还有版本)的订阅者,如果适合您,您可以阅读事件的完整原始文档,对其进行处理、计算和更新投影。如果该活动不适合您,您只需跳过它。

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

好吧...

所有这些在我看来都是合理的。直到这里都没有消息。

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

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

2 个同等重要的不同聚合的示例:

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

Locomotive
Wagon
Run Code Online (Sandbox Code Playgroud)

试想一下,一个机车可以运载 0 或 1 节车厢,但没有多少车厢。

你有这些命令:

Attach( locomotiveId, wagonId )
Detach( locomotiveId, wagonId )
Run Code Online (Sandbox Code Playgroud)

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

这些事件显然是相应的:

AttachedEvent( locomotiveId, wagonId )
DetachedEvent( locomotiveId, wagonId )
Run Code Online (Sandbox Code Playgroud)

问:

那里的流 ID 是什么?机车和马车同等重要,它不是“机车”或“马车”的事件。这是影响这两者的域事件!哪一个是streamId,为什么?

具有 2 个相同类型的聚合的示例

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

Issue
Run Code Online (Sandbox Code Playgroud)

以及这些命令:

MarkAsRelated( issueAId, issueBId )
UnmarkAsRelated( issueAId, issueBId )
Run Code Online (Sandbox Code Playgroud)

如果标记已经存在,则标记被拒绝,如果之前没有任何标记,则取消标记被拒绝。

还有那些事件:

MarkedAsRelatedEvent( issueAId, issueBId )
UnmarkedAsRelatedEvent( issueAId, issueBId )
Run Code Online (Sandbox Code Playgroud)

问:

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

历史只写一次

在任何情况下,我都没有看到为每个事件创建两个事件。那是计算器的问题...

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

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

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

Ebe*_*oux 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

事件流是事件驻留的Locomotive地方。WagonAttached总量以何种方式Wagon依赖于这一事件是有争议的。我认为马车可能不太关心,就像 aProduct不太关心它与哪个Order(可能在这种情况下)相关一样。聚合是似乎更适合关联实体Order的一面。我猜你的机车与货车的关系可能会遵循相同的模式,因为机车会连接不止一辆货车。可能与设计有关,但我假设这些都是假设的例子。OrderItem

这同样适用于Issue. 如果一个人可以附加多个,那么OrdertoProduct概念就会发挥作用。即使涉及两个问题,也存在某种方向,因为其中一个问题作为从属问题附属于主要问题。也许事件带有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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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