【问题标题】:How to deal with Variable data over time in associations如何处理关联中随时间变化的变量数据
【发布时间】:2016-11-15 04:43:53
【问题描述】:

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

  • 啤酒是在哪里买的?
  • 获取饮料交易 => 获取其服务员 => 获取此服务员的餐厅:这是购买啤酒的地方
  • 所以在时间 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
    

    但是我们知道第一笔交易是在凯撒的宫殿里进行的!


    解决方案 1

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

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

    缺点:每个模型的每次修改都会复制内容,随着时间的推移,这会做很多事情。

    解决方案 2

    在您创建事务时保留关联模型的当前状态副本。

    好处:我不会复制内容。

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


    你的解决方案是什么?


    [编辑]

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

  • 这个订单的金额是多少?
  • 获取饮料交易 => 获取其产品 => 获取该产品的价格 => 乘以订单数量:这是订单的总金额
  • 所以在时间 T,当我显示所有交易时,我会按照关联获取我的数据,因此我可以显示这个:

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

    ==> 订单数量 n°1 : 6.

    现在假设啤酒的价格为 2.5。

    如果我刷新这张表,结果将是

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

    ==> 订单数量 n°1 : 5.

    所以,再一次,有两种解决方案可用:当它的价格改变时,我是否克隆啤酒产品?下订单时,我是否会在我的订单中保存一份啤酒?你有第三种解决方案吗?

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

    【问题讨论】:

    • @Close Voters:我理解这可以被视为基于意见,但实际的问题是“如何保存和检索关联对象的历史数据?”这可以客观地回答。这个问题只需要重新措辞。

    标签: data-modeling


    【解决方案1】:

    事件溯源

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

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

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

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

    Lambda 架构

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

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

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

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

    服务层允许通过将多个批处理结果和速度层结果组合成一个查询来进行更高级的查询。例如,您可以通过将月收入相加来计算当年的收入。

    但如前所述,仅当您需要频繁且快速的数据时才使用 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 获取餐厅名称

    您可以看到它变得复杂,因此您应该只保留有用数据的历史记录。

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

    总结

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

    【讨论】:

    • 非常感谢克里斯蒂安。我将实施此解决方案,因为 lambda 架构似乎与我的应用程序特别相关(我需要快速提取餐馆的统计信息),即使“决定要保留历史记录的数据”部分很麻烦我有点:我宁愿 not 必须选择。
    • @GregoryKapustin:如果您有足够的时间为所有事情实施事件溯源,那么您不必做决定。但通常你必须找到妥协。另一种选择是跳过批处理层。将所有内容保存为事件并仅在速度层中计算当前状态(如在传统应用程序中一样),然后对事件运行临时查询以获取历史数据。
    【解决方案2】:

    我喜欢这个问题,因为它提出了一些非常直截了当的问题。

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

    服务员案例

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

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

    要实现这一点,您必须通过引入员工的概念来从作为服务员的 Julius 中抽象出来。朱利叶斯是一名工作人员。工作人员担任服务员。在餐厅 1 工作时,朱利叶斯是服务员 A,而当他在另一家餐厅工作时,他是服务员 B,但始终是同一位员工——朱利叶斯。使用实体“员工”可以轻松回答查询。

    上行空间: 不会丢失历史数据或过度重复。

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

    总而言之 - 抽象数据主体将更改为新实体并从交易中引用它。

    订单案例价值

    涉及更多关于“此订单的价值是什么”的扩展用例。我从事跨币种交易,价格表中观察者(用户)的价值会随着币种波动的发生而每天发生变化。

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

    解决方法是保存到订单行中:

    1. 以客户货币计的产品价值,
    2. 或自定义货币和供应商货币之间的汇率,
    3. 但理想情况下两者都做以避免舍入错误。

    这样做的目的是提供“在下订单之日,第 1 行的价格为 44.56 美元,汇率为 1.1 美元/英镑”。锁定这些数据后,您可以根据客户的期望开具发票,并随着时间的推移提供一致的支出报告。

    上行空间:一致的历史数据。快速的数据库性能,无需针对历史汇率表进行查找。

    缺点:一些数据重复。然而,用历史汇率存储和索引来权衡存储和索引的开销,那么这可能是一个好处。

    关于在您的订单表中添加“金额” - 如果您想获得一致的数据历史记录,您必须这样做。如果您只使用一种货币,那么金额是唯一的额外存储问题。通过添加这一属性,您可以保护历史。您的另一种选择是存储饮料的历史成本表,因此您知道 1 月份啤酒是 1 美元,2 月份是 1.10 美元等,然后将成本表密钥存储在事务中,这样如果有人询问,您就可以查找成本一个历史性的秩序。但是存储密钥加上使这可行所需的索引的开销将超过将“金额”克隆到订单记录的存储成本。

    总而言之 - 克隆成本数据会随时间变化。

    【讨论】:

    • 接近我列出的 2 种解决方案的混合,我需要像“员工”或“菜单”(链接酒吧和产品)这样的转换表,以及在某处保存一些重要字段,如我所说,以 JSON 格式或其他字段(我将在其他模型中执行,例如“orderData”)。仍然......令我惊讶的是,如今,我们被这两种解决方案所困扰,我认为这两种解决方案都不完美...... (数据增长 + 数据重复 + 字符串化索引或很多字段)
    • 如果您的应用程序将有超过 10 条记录,1000 条记录,您需要务实。它们使网络更快,磁盘更便宜,因此重点放在您在数据库设计中做出的决策上,这些决策最终将成为效率驱动因素。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-02-28
    • 2021-02-06
    • 1970-01-01
    • 2016-12-21
    • 1970-01-01
    • 2017-07-13
    • 1970-01-01
    相关资源
    最近更新 更多