如何在关联

时间:2016-11-10 11:41:39

标签: data-modeling

在链接模型中(比如饮料交易,服务员和餐馆),当您想要显示数据时,您会在链接的内容中查找信息:

  • 啤酒在哪里买?
  • Fetch Drink transaction =>获取其Waiter =>拿这个服务员的餐厅:这是购买啤酒的地方
  • 所以在时间T,当我显示所有事务时,我会在关联后获取数据,因此我可以显示:

    TransactionID   Waiter   Restaurant
    1               Julius   Caesar's palace
    2               Cleo     Moe's tavern
    

    现在让我们说我的服务员搬到另一家餐馆。

    如果我刷新此表,结果将是

    TransactionID   Waiter   Restaurant
    1               Julius   Moe's tavern
    2               Cleo     Moe's tavern
    

    但我们知道n°1的交易是在凯撒宫完成的!

    解决方案1 ​​

    不要修改服务员Julius,而是克隆它。

    上升:我保持模型之间的关联,并且仍然可以过滤每个相关模型的每个字段。

    下行:每个模型的每次修改都会复制内容,这些内容可以在时间过后完成。

    解决方案2

    创建交易时,请保留相关模型的当前状态的副本。

    上行:我不会复制内容。

    下行:您不能再在内容上使用字段来显示,排序或过滤原始数据在里面,比方说,一个JSON字段。因此,如果您使用MySQL,则必须通过该字段中的makin普通搜索查询来过滤数据。

    你的解决方案是什么?

    [编辑]

    问题更进一步,因为不仅仅是关联改变时的问题:对关联模型的简单修改也会导致问题。 我的意思是:

  • 此订单的金额是多少?
  • Fetch Drink transaction =>获取其产品=>获取此商品的价格=>按订单数量乘以:这是订单的总金额
  • 所以在时间T,当我显示所有事务时,我会在关联后获取数据,因此我可以显示:

    TransactionID   Qty   ProductId
    1               2     1
    
    ProductID   Title   Price
    1           Beer    3
    

    ==>订单金额为1:6。

    现在让我们说啤酒价格为2.5。

    如果我刷新此表,结果将是

    TransactionID   Qty   ProductId
    1               2     1
    
    ProductID   Title   Price
    1           Beer    2,5
    

    ==>订单金额为1:5。

    所以,再次提供了两种解决方案:当价格发生变化时,我会克隆啤酒产品吗?订单生成后,我会在订单中保存一份啤酒吗?你有第三个解决方案吗?

    我不能在我的订单上添加“amount”属性:是的,它可以解决这个问题(部分),但它不是可扩展的解决方案,因为许多其他属性将处于相同的情况,我无法将属性相乘像这样。

    2 个答案:

    答案 0 :(得分:1)

    活动采购

    这是Event Sourcing的一个很好的用例。 Martin Fowler写了一篇非常好的文章,我建议你阅读它。

      

    有时我们不仅想知道我们在哪里,我们也想知道我们是如何到达那里的。

    我们的想法是永远不会覆盖数据,而是为您想要保留历史记录的所有内容创建不可变的事务。在您的情况下,您将拥有WaiterRelocationEventPriceChangeEvent s。您可以按顺序应用每个事件来重新创建任何给定时间的状态。

    如果您不使用事件采购,则会丢失信息。通常,忘记历史信息是可以接受的,但有时却不是。

    Lambda架构

    由于您不想重新计算每个请求的所有内容,因此建议您实施Lambda Architecture。该体系结构通常用BigData技术和框架解释,但您可以使用Plain Old Java和CronJobs实现它。

    它由三部分组成:批处理层服务层速度层

    批处理层会定期计算数据的汇总版本,例如,您每天计算一次月收入。所以当月的收入每晚都会变化,直到月结束。

    但现在你想要实时了解收入。因此,您添加一个速度图层,它将立即应用当前日期的所有事件。现在,如果当前月份收入的请求到达,您将累加批处理层速度层的最后结果。

    服务层通过将多个批处理结果和 Speed Layer 结果合并为一个查询,允许更高级的查询。例如,您可以通过汇总月收入来计算年度收入。

    但如前所述,如果您需要经常且快速的数据,则只使用Lambda方法,因为它会增加额外的复杂性。很少需要的计算应该是即时运行的。例如:哪个服务员在周六晚上创造了最多的收入?

    示例

    Restaurants:
    | Timestamp  | Id | Name            |
    | ---------- | -- | --------------- |
    | 2016-01-01 |  1 | Caesar's palace |
    | 2016-11-01 |  2 | Moe's tavern    |
    
    Waiters:
    | Timestamp  | Id | Name     | FirstRestaurant |
    | ---------- | -- | -------- | --------------- |
    | 2016-01-01 | 11 | Julius   |               1 |
    | 2016-11-01 | 12 | Cleo     |               2 |
    
    WaiterRelocationEvents:
    | Timestamp  | WaiterId | RestaurantId |
    | ---------- | -------- | ------------ |
    | 2016-06-01 |       11 |            2 |
    
    Products:
    | Timestamp  | Id | Name     | FirstPrice |
    | ---------- | -- | -------- | ---------- |
    | 2016-01-01 | 21 | Beer     |       3.00 |
    
    PriceChangeEvent:
    | Timestamp  | ProductId | NewPrice |
    | ---------- | --------- | -------- |
    | 2016-11-01 |        21 |     2.50 |
    
    Orders:
    | Timestamp  | Id | ProductId | Quantity | WaiterId |
    | ---------- | -- | --------- | -------- | -------- |
    | 2016-06-14 | 31 |        21 |        2 |       11 |
    

    现在让我们获取有关订单31的所有信息。

    1. 获得订单31
    2. 2016-06-14获得产品21的价格
      • 在日期之前获得最后的PriceChangeEvent,或者如果不存在则使用FirstPrice
    3. 通过将检索到的价格与数量相乘来计算总价格
    4. 得到服务员11
    5. 2016-06-14获得服务员的餐厅
      • 在日期之前获取最后一个WaiterRelocationEvent或使用FirstRestaurant(如果不存在)
    6. 通过检索服务员的餐馆ID获取餐厅名称
    7. 正如您所看到的那样变得复杂,因此您应该只保留有用数据的历史记录。

      • 我不会在计算中涉及重定位事件。它们可以存储,但我会将餐馆ID和服务员ID直接存储在订单中。
      • 另一方面,价格历史可能很有趣,以检查价格变化后订单是否下降。在这里,您可以使用 Lambda Architecure 计算包含原始订单和价格历史价格的完整订单。

      <强>摘要

      • 确定要保留历史记录的数据。
      • 为该数据实施事件采购
      • 使用 Lambda架构加速常用查询。

    答案 1 :(得分:0)

    我喜欢这个问题,因为它提出了一些非常简单的东西,也有一些更微妙的东西。

    两种情况下的共同原则是“历史记录不得更改”,这意味着如果我们今天在指定的过去日期范围内运行查询,结果与我们在将来的任何时间点运行相同查询时的结果相同。

    服务员案例

    当服务员更换餐馆时,我们不得更改销售历史。如果服务员朱利叶斯昨天在餐厅1卖了一杯酒,那么他今天在餐厅2换掉了更多饮料,我们必须保留这些细节。

    因此,我们希望能够回答诸如“Julius在餐厅1中销售了多少饮料”以及“Julius在所有餐厅销售了多少饮料”等问题。

    要实现这一目标,你必须通过引入员工的概念来抽象朱利叶斯作为服务员。朱利叶斯是员工。工作人员作为服务员工作。在餐厅工作时,朱利叶斯是服务员A,当他在另一家餐馆工作时,他是服务员B,但总是同一名员工 - 朱利叶斯。通过实体“员工”,可以轻松地回答查询。

    上行: 不丢失历史数据或重复过多。

    下行新实体员工必须受到管理。但是服务员表内容减少了,使得数据存储的净开销很低。

    总之 - 抽象数据可以更改为新实体并从事务中返回。

    订单案例的价值

    更多涉及“此订单的价值是什么”的扩展用例。我在交叉货币交易中工作,当货币波动发生时,价格表中观察者(用户)的价值每天都在变化。

    但是有充分的理由来锁定订单价值。例如,发票处理系统对其预期发票金额与提交的发票金额之间的微小差异具有容差,但任何大的差异都可能导致延迟付款,而发票处理人员会检查问题。此外,如果客户运行有关其历史购买的报告,那么这些订单的价值必须保持一致,尽管货币汇率随时间波动。

    解决方案是保存在订单行中:

    1. 以客户货币计算的产品价值
    2. 或自定义和供应商货币之间的比率,
    3. 但理想情况下两者都要避免舍入错误。
    4. 这样做是为了声明'在该订单下单第1行的成本为44.56美元,汇率为1.1美元/英镑。锁定此数据后,您可以按照客户的期望开具发票,并随时提供一致的支出报告。

      上行:一致的历史数据。快速的数据库性能,因为历史汇率表不需要查找。

      下行:部分数据重复。然而,对于历史存储率和指数化的存储和指数化的开销进行折衷,那么这可能是一个好处。

      关于向订单表添加“金额” - 如果要实现一致的数据历史记录,则必须执行此操作。如果您只使用一种货币,那么金额是唯一额外的存储问题。通过添加此属性,您可以保护历史记录。您的另一种选择是存储饮料的历史成本表,以便您知道1月啤酒是1美元,2月它是1.10美元等,然后将成本表密钥存储在交易中,以便您可以查询成本,如果有人询问历史性的秩序。但是,存储密钥PLUS所需的索引使得这种可行性所需的开销将超过将“数量”克隆到订单记录上的存储成本。

      总结 - 克隆成本数据将随时间变化。