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

我正在研究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")

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

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

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

(另请注意:我们在这里删除了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}
    ]
}
点赞