当域事件影响同一有界上下文中的多个聚合时,EventSourcing中的StreamId是什么?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了当域事件影响同一有界上下文中的多个聚合时,EventSourcing中的StreamId是什么?相关的知识,希望对你有一定的参考价值。
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
那么当事件影响当时的多个实体时,你如何处理事件流呢?并不是它“属于”一个而另一个是对它的补充,但它真的等于两者?
会发生什么...如果事件对两个不同类型的聚合“同等重要”(假设它们在同一个有界上下文中)或者甚至它引用了相同聚合类型的两个实例
event-sourcing是一个简单的(注意:不容易)的想法。我们将聚合保存到稳定存储时,而不是覆盖以前的状态,而是编写一个新版本,链接回以前的版本。此外,我们不是写出新版本的整个副本,而是写出差异,并且差异以特定于域的方式表达。
因此,将聚合保存到流类似于将聚合的表示形式保存为键值存储中的文档,或者保存为关系数据库中的行。
当您询问它属于哪个“流”时:它属于更改的聚合流,就像在其他任何存储策略中一样。
如果您不确定哪个聚合发生了变化,那么您所拥有的是建模问题,而不是事件来源问题。
您的两个示例都描述了在两个聚合之间引入关系;它类似于在数据库中的两个表之间建立多对多的关系。谁拥有M2M表?
好吧,如果聚合都不需要该信息来确保其自身的不变性,那么M2M表可能本身就是一个聚合。
想象一下两方之间的合同代表 - 可能会发现双方是偶然的,而“合同”是一个重要的想法,值得建模为自己的事物。
如果关系明显是“一部分”的一部分(该聚合是保护依赖于关系状态的不变量),那么该聚合将负责编辑新表,而另一个聚合将忽略它。
如果两个聚合都关心关系,那么你有两个问题之一
1)您对域名的分析是错误的 - 您在错误的地方绘制了聚合边界。把你带到白板并开始画出来。
2)您有两个关系副本 - 每个聚合一个副本,但这些副本不一定相互一致。
这是一个重要的启发式方法:如果你真的有两个不同的聚合,你应该能够将它们存储在两个完全不同的数据库中。他们无法共享彼此的数据,但他们可以保留自己的其他人数据的版本/时间戳/缓存副本。
因此,左手聚合进行了更改,“管道”将“左手聚合更改”消息发送到右手聚合,然后右手聚合更新其缓存。
请注意,在我们认为合同是管理其自身状态的头等问题的情况下,这将如何工作。模型更新合同,将更改保存到其状态,然后管道出现并将更改的副本传递给左手聚合和右手聚合。
Simple。不一定容易。
我不认为它与事件采购本身有任何关系。也许设计可以修改一下。
我会为这个机车选择这样的东西:
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
也是如此。如果一个人可以附加多个,那么Order
到Product
概念就会发挥作用。即使涉及两个问题,也有一个方向,因为作为下属的一个问题与主要问题相关。也许有RelationshipType
的事件,例如Dependency
,Impediment
等。在这种情况下,可能会使用值对象来表示:
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;
}
}
关键是事件属于单个聚合但仍然代表关系。你只需要确定最有意义的一面。
事件可以(或应该)使用一些事件驱动的体系结构方法(比如服务总线)发布,以便通知其他感兴趣的各方。
以上是关于当域事件影响同一有界上下文中的多个聚合时,EventSourcing中的StreamId是什么?的主要内容,如果未能解决你的问题,请参考以下文章