projection - 事件溯源 : denormalizing relationships in projections

标签 projection cqrs event-sourcing denormalization

我正在研究 CQRS/ES 架构。我们将多个异步投影并行运行到读取存储中,因为某些投影可能比其他投影慢得多,并且我们希望与写入端保持更多同步以实现更快的投影。

我试图了解有关如何生成读取模型以及这可能需要多少数据重复的方法。

让我们以带有商品的订单为例。一个订单可以有多个项目,每个项目都有一个名称。项目和订单是单独的聚合。

我可以尝试以更规范化的方式保存读取模型,在那里我为每个项目和订单创建一个实体或文档,然后引用它们 - 或者我可能想以更非规范化的方式保存它,我有订单其中包含项目。

标准化

{
  Id: Order1,
  Items: [Item1, Item2]
}

{
  Id: Item1,
  Name: "Foosaver 9000"
}

{
  Id: Item2,
  Name: "Foosaver 7500"
}

使用更规范化的格式将允许单个投影处理影响/影响项目和订单的事件并更新相应的对象。这也意味着对项目名称的任何更改都会影响所有订单。例如,客户可能会收到与相应发票不同的项目的交货单(因此显然该模型可能不够好并导致我们遇到与非规范化相同的问题......)

非规范化
{
  Id: Order1,
  Items: [
    {Id: Item1, Name: "Foosaver 9000"},
    {Id: Item2, Name: "Foosaver 7500"},
  ]
}

然而,非规范化需要一些来源,我可以在其中查找当前相关数据 - 例如项目。这意味着我要么必须传输事件中可能需要的所有信息,要么必须跟踪我为非规范化提供的数据。这也意味着我可能必须为每个投影执行一次 - 即我可能需要一个非规范化 ItemForOrder 以及非规范化 ItemForSomethingElse - 两者都只包含每个非规范化实体或文档所需的最低限度的属性(无论何时创建或修改)。

如果我愿意分享相同的 项目 在读取存储中,我最终可能会混合不同时间点的项目定义,因为项目和订单的预测可能不会以相同的速度运行。在最坏的情况下,项目的投影可能尚未创建我需要为其属性提供源的项目。

通常,在处理来自事件流的关系时,我有哪些方法?

更新 2016-06-17

目前,我正在通过为每个非规范化读取模型及其相关数据运行单个投影来解决这个问题。如果我有多个读取模型必须共享相同的相关数据,那么我可能会将它们放入同一个投影中,以避免重复查找所需的相同相关数据。

这些相关模型甚至可能在某种程度上被标准化、优化,但我必须访问它们。我的投影是唯一读取和写入它们的东西,所以我确切地知道它们是如何读取的。
// related data 
public class Item 
{
  public Guid Id {get; set;}
  public string Name {get; set;}
  /* and whatever else is needed but not provided by events */
}

// denormalised info for document
public class ItemInfo 
{
  public Guid Id {get; set;}
  public string Name {get; set;}
}

// denormalised data as document
public class ItemStockLevel
{
  public ItemInfo Item {get; set;} // when this is a document
  public decimal Quantity {get; set;}
}

// or for RDBMS
public class ItemStockLevel
{
  public Guid ItemId {get; set;}
  public string ItemName {get; set;}
  public decimal Quantity {get; set;}
}

然而,这里更隐蔽的问题是何时更新哪些相关数据。这在很大程度上取决于业务流程。

例如,我不想在下订单后更改订单的项目描述。当投影处理事件时,我必须只更新根据业务流程更改的数据。

因此,可以将这些信息放入事件中(并使用客户端发送的数据?)。如果我们发现我们稍后需要额外的数据,那么我们可能不得不退回到从事件流中投影相关数据并从那里读取它......

对于纯 CQRS 架构,这可以看作是一个类似的问题:什么时候更新文档中的非规范化数据?在将数据呈现给用户之前,您何时刷新数据?同样,业务流程可能会插入这个决定。

最佳答案

首先,我认为您要谨慎对待生命周期的聚合。在通常的购物车域中,购物车(订单)生命周期跨越商品的生命周期。 Udi Dahan 写道 Don't Create Aggregate Roots ,我发现这意味着聚合持有对“创建”它们的聚合的引用,而不是相反。

因此,我希望事件历史看起来像

// Assuming Orders come from Customers
OrderCreated(orderId: Order1, customerId: Customer1)

ItemAdded(itemId: Item1, orderId: Order1, Name:"Foosaver 9000")

ItemAdded(itemId: Item2, orderId: Order1, Name:"Foosaver 7500")

现在,仍然没有关于排序的保证 - 这将取决于在写入模型中如何设计聚合,您的事件存储是否跨不同历史记录线性化事件,等等。

请注意,在您的规范化 View 中,您可以从顺序转到项目,但不能反过来。处理我所描述的事件给了你同样的限制:不是带有神秘元素的订单,而是带有神秘订单的元素。任何查找订单的人要么还没有看到它,要么看到它是空的,要么看到它有一定数量的商品;并且可以跟踪从这些项目到 keystore 的链接。

您的键值存储中的规范化表单不需要从您的示例中更改;负责编写规范化订单形式的投影也需要足够智能以观看项目流,但这一切都很好。

(另请注意:我们在这里省略了 ItemRemoved)

没关系,但它忽略了读取比写入更频繁的想法。对于热查询,您将需要非规范化形式可用:存储中的数据是您要发送以响应查询的 DTO。例如,如果查询支持订单报告(不允许编辑),那么您也不需要发送商品 ID。
{
    Title: "Your order #{Order1}",
    Items: [
        {Name: "Foosaver 9000"},
        {Name: "Foosaver 7500"}
    ]
}

您可能需要考虑的一件事是跟踪相关聚合的版本,以便当用户从一个 View 导航到下一个 View 时——而不是获得过时的投影,查询会暂停,等待新的投影 catch 。

例如,如果您的 DTO 是超媒体,那么它可能看起来像
{
    Title: "Your order #{Order1}",
    refreshUrl: /orders/Order1?atLeastVersion=20,
    Items: [
        {Name: "Foosaver 9000", detailsUrl: /items/Item1?atLeastVersion=7},
        {Name: "Foosaver 7500", detailsUrl: /items/Item2?atLeastVersion=9}
    ]
}

关于projection - 事件溯源 : denormalizing relationships in projections,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37437475/

相关文章:

architecture - 现有关系数据库上的 Cqrs 和事件溯源

database - 使用 NATS Streaming Server 作为物联网位置数据的主要数据存储?

domain-driven-design - 当仍然存在无用事件或撤消的可能性时,您是否立即将事件应用于域模型?

c++ - 将投影转换为正交光线转换

design-patterns - 重播事件以进行调整

android - 如何在 ogl 中获取触摸屏点的坐标?

c# - 使用 CQRS 方法处理并发

concurrency - Event Sourcing/CQRS对聚合、原子性、并发和最终一致性的疑惑

sql - 如何使用 Redshift 预测 future 日期

java - 从 objectify 中的投影查询中提取值