事件采购:在预测中对关系进行非规范化

时间:2016-05-25 12:40:33

标签: 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架构的类似问题:何时更新文档中的非规范化数据?什么时候在将数据呈现给用户之前刷新数据?同样,业务流程可能会推动这一决定。

2 个答案:

答案 0 :(得分:1)

首先,我认为您希望在关于生命周期的聚合中小心。在通常的购物车域中,购物车(订单)生命周期跨越项目的生命周期。 Udi Dahan写了Don't Create Aggregate Roots,我发现这意味着聚合数字会引用“#34;创建的聚合”。他们,而不是相反。

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

// 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")

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

请注意,在规范化视图中,您可以从订单转到商品,但不是相反。处理我所描述的事件会给你同样的限制:你有神秘物品的订单,而不是带有神秘物品的订单。任何寻找订单的人要么看不到它,要么看空,要么看到它有一些项目;并且可以跟踪从这些项目到密钥库的链接。

您的键值商店中的规范化表单无需更改示例;负责编写规范化订单形式的投影需要足够聪明才能观察物品流,但这一切都很好。

(另请注意:我们在这里删除了ItemRemoved)

没关系,但它错过了读取比写入更频繁的想法。对于热查询,您将希望非规范化表单可用:存储中的数据是您要响应查询而发送的DTO。例如,如果查询支持订单报告(不允许编辑),那么您也不需要发送项目ID。

{
    Title: "Your order #{Order1}",
    Items: [
        {Name: "Foosaver 9000"},
        {Name: "Foosaver 7500"}
    ]
}

您可能会考虑的一件事是跟踪相关聚合的版本,以便当用户从一个视图导航到另一个视图时 - 而不是获得陈旧的投影,查询会暂停等待新投影捕获起来。

例如,如果您的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}
    ]
}

答案 1 :(得分:0)

我也有这个问题并尝试过不同的事情。我读过this suggestion虽然我还没试过,但我认为这可能是最好的方法。只需在发布之前丰富事件。