如何处理关联中随时间变化的数据
How to deal with Variable data over time in associations
在链接模型中(比如饮料交易、服务员和餐厅),当您想要显示数据时,您会在链接内容中查找信息:
啤酒是在哪里买的?
获取饮料交易 => 获取其服务员 => 获取该服务员的餐厅:这是购买啤酒的地方
所以在时间 T,当我显示所有交易时,我根据关联获取我的数据,因此我可以显示这个:
TransactionID Waiter Restaurant
1 Julius Caesar's palace
2 Cleo Moe's tavern
假设现在我的服务员搬到了另一家餐馆。
如果我刷新这个table,结果会是
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。
如果我刷新这个table,结果会是
TransactionID Qty ProductId
1 2 1
ProductID Title Price
1 Beer 2,5
==> 订单数量 n°1 : 5.
那么,这 2 种解决方案再次可用:当啤酒产品的价格发生变化时,我是否要克隆它?下订单时我会在订单中保存一份啤酒吗?你有第三种解决方案吗?
我不能只在我的订单上添加一个 "amount" 属性:是的,它可以(部分)解决这个问题,但它不是一个可扩展的解决方案,因为许多其他属性将处于相同的情况,我可以' 像这样乘以属性。
我喜欢这个问题,因为它提出了一些非常直截了当的问题,也提出了一些更微妙的问题。
这两种情况的共同原则是“历史不能改变”,这意味着如果我们今天 运行 查询指定的过去日期范围,结果与我们 运行 时的结果相同将来任何时候的相同查询。
服务员案例
当服务员更换餐厅时,我们不得更改销售历史记录。如果服务员 Julius 昨天在 1 号餐厅卖了一种饮料,然后他今天转而在 2 号餐厅卖更多的饮料,我们必须保留这些细节。
因此,我们希望能够回答诸如“Julius 在餐厅 1 中售出了多少酒”和“Julius 在所有餐厅中售出了多少酒”之类的问题。
要实现这一点,您必须通过引入员工的概念,将 Julius 作为服务员抽象出来。朱利叶斯是一名工作人员。工作人员担任服务员。在 1 号餐厅工作时,朱利叶斯是服务员 A,在另一家餐厅工作时,他是服务员 B,但始终是同一名员工——朱利叶斯。使用实体“员工”可以轻松回答查询。
上升空间:
历史数据不会丢失或重复过多。
缺点 必须管理新实体员工。但是 waiter table 内容减少使得数据存储的净开销很低。
总而言之 - 将要更改为新实体的数据抽象化并从事务中引用它。
订单案例价值
关于“此订单的价值是什么”的扩展用例涉及更多。我从事跨货币交易,随着货币波动的发生,价目表中观察者(用户)的价值每天都在变化。
但是有充分的理由将订单值锁定到位。例如,发票处理系统可以容忍其预期发票价值与提交发票价值之间的微小差异,但任何较大差异都可能导致延迟付款,同时发票处理程序会检查问题。此外,如果客户 运行 报告他们的历史购买情况,那么尽管货币汇率随时间波动,但这些订单的价值必须保持一致。
解决方法是存入订单行:
- 以客户货币表示的产品价值,
- 或自定义货币与供应商货币之间的汇率,
- 但最好两者都做以避免舍入错误。
这样做是提供一个声明“在下订单的日期,第 1 行的价格为 44.56 美元,汇率为 1.1 美元/英镑”。锁定此数据后,您可以根据客户的期望开具发票,并随着时间的推移提供一致的支出报告。
上行空间: 历史数据一致。快速的数据库性能,因为不需要根据历史速率 tables.
进行查找
缺点:一些数据重复。然而,权衡历史利率存储和索引的存储和索引开销,这可能是一个好处。
关于将 'amount' 添加到您的订单 table - 如果您想获得一致的数据历史记录,则必须这样做。如果您只使用一种货币,那么金额是唯一额外的存储问题。通过添加这一属性,您已经保护了历史。您的另一种选择是存储饮料的历史成本 table,这样您就知道 1 月份啤酒的价格为 1 美元,2 月份为 1.10 美元等,然后在交易中存储成本-table 键,以便您可以如果有人询问历史订单,请查看成本。但是存储密钥的开销加上使此可行所需的索引将超过将 'amount' 克隆到订单记录的存储成本。
总而言之 - 克隆成本数据会随时间变化。
事件溯源
这是 Event Sourcing 的一个很好的用例。 Martin Fowler 写了一篇很好的文章,建议你看看。
there are times when we don't just want to see where we are, we also want to know how we got there.
我们的想法是永远不要覆盖数据,而是为您想要保留历史记录的所有内容创建不可变的事务。在您的情况下,您将拥有 WaiterRelocationEvent
s 和 PriceChangeEvent
s。您可以通过按顺序应用每个事件来重新创建任何给定时间的状态。
如果您不使用 事件溯源,您将丢失信息。忘记历史信息通常是可以接受的,但有时则不然。
Lambda 架构
由于您不想对每个请求都重新计算所有内容,因此建议实施 Lambda Architecture。该架构通常用 BigData 技术和框架来解释,但您可以使用 Plain Old Java 和 CronJobs 来实现它。
它由三部分组成:批处理层、服务层和速度层。
批处理层 定期计算数据的聚合版本,例如,您将每天计算一次月收入。所以当月的收入每天晚上都会变化,直到一个月结束。
但是现在您想实时了解收入。因此,您添加了一个速度层,它将立即应用当前日期的所有事件。现在,如果收到当月收入请求,您将把 Batch Layer 和 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 的所有信息。
- 获取订单 31
- 获取产品21在2016-06-14的价格
- 获取日期之前的最后一个 PriceChangeEvent 或如果 none 存在则使用 FirstPrice
- 通过将检索到的价格乘以数量来计算总价
- 获取服务员 11
- 2016-06-14获取服务员餐厅
- 获取日期之前的最后一个 WaiterRelocationEvent 或如果 none 存在则使用 FirstRestaurant
- 通过检索服务员的餐厅 ID 获取餐厅名称
如您所见,它变得很复杂,因此您应该只保留有用数据的历史记录。
- 我不会在计算中涉及搬迁事件。它们可以存储,但我会直接在订单中存储餐厅 ID 和服务员 ID。
- 另一方面,价格历史记录可能很有趣,可以用来检查价格变动后订单是否下降。在这里,您可以使用 Lambda Architecure 来计算包含原始订单价格和价格历史记录的完整订单。
总结
- 决定要保留哪些数据的历史记录。
- 为该数据实施事件溯源。
- 使用 Lambda 架构 加速常用查询。
在链接模型中(比如饮料交易、服务员和餐厅),当您想要显示数据时,您会在链接内容中查找信息:
所以在时间 T,当我显示所有交易时,我根据关联获取我的数据,因此我可以显示这个:
TransactionID Waiter Restaurant
1 Julius Caesar's palace
2 Cleo Moe's tavern
假设现在我的服务员搬到了另一家餐馆。
如果我刷新这个table,结果会是
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。
如果我刷新这个table,结果会是
TransactionID Qty ProductId
1 2 1
ProductID Title Price
1 Beer 2,5
==> 订单数量 n°1 : 5.
那么,这 2 种解决方案再次可用:当啤酒产品的价格发生变化时,我是否要克隆它?下订单时我会在订单中保存一份啤酒吗?你有第三种解决方案吗?
我不能只在我的订单上添加一个 "amount" 属性:是的,它可以(部分)解决这个问题,但它不是一个可扩展的解决方案,因为许多其他属性将处于相同的情况,我可以' 像这样乘以属性。
我喜欢这个问题,因为它提出了一些非常直截了当的问题,也提出了一些更微妙的问题。
这两种情况的共同原则是“历史不能改变”,这意味着如果我们今天 运行 查询指定的过去日期范围,结果与我们 运行 时的结果相同将来任何时候的相同查询。
服务员案例
当服务员更换餐厅时,我们不得更改销售历史记录。如果服务员 Julius 昨天在 1 号餐厅卖了一种饮料,然后他今天转而在 2 号餐厅卖更多的饮料,我们必须保留这些细节。
因此,我们希望能够回答诸如“Julius 在餐厅 1 中售出了多少酒”和“Julius 在所有餐厅中售出了多少酒”之类的问题。
要实现这一点,您必须通过引入员工的概念,将 Julius 作为服务员抽象出来。朱利叶斯是一名工作人员。工作人员担任服务员。在 1 号餐厅工作时,朱利叶斯是服务员 A,在另一家餐厅工作时,他是服务员 B,但始终是同一名员工——朱利叶斯。使用实体“员工”可以轻松回答查询。
上升空间: 历史数据不会丢失或重复过多。
缺点 必须管理新实体员工。但是 waiter table 内容减少使得数据存储的净开销很低。
总而言之 - 将要更改为新实体的数据抽象化并从事务中引用它。
订单案例价值
关于“此订单的价值是什么”的扩展用例涉及更多。我从事跨货币交易,随着货币波动的发生,价目表中观察者(用户)的价值每天都在变化。
但是有充分的理由将订单值锁定到位。例如,发票处理系统可以容忍其预期发票价值与提交发票价值之间的微小差异,但任何较大差异都可能导致延迟付款,同时发票处理程序会检查问题。此外,如果客户 运行 报告他们的历史购买情况,那么尽管货币汇率随时间波动,但这些订单的价值必须保持一致。
解决方法是存入订单行:
- 以客户货币表示的产品价值,
- 或自定义货币与供应商货币之间的汇率,
- 但最好两者都做以避免舍入错误。
这样做是提供一个声明“在下订单的日期,第 1 行的价格为 44.56 美元,汇率为 1.1 美元/英镑”。锁定此数据后,您可以根据客户的期望开具发票,并随着时间的推移提供一致的支出报告。
上行空间: 历史数据一致。快速的数据库性能,因为不需要根据历史速率 tables.
进行查找缺点:一些数据重复。然而,权衡历史利率存储和索引的存储和索引开销,这可能是一个好处。
关于将 'amount' 添加到您的订单 table - 如果您想获得一致的数据历史记录,则必须这样做。如果您只使用一种货币,那么金额是唯一额外的存储问题。通过添加这一属性,您已经保护了历史。您的另一种选择是存储饮料的历史成本 table,这样您就知道 1 月份啤酒的价格为 1 美元,2 月份为 1.10 美元等,然后在交易中存储成本-table 键,以便您可以如果有人询问历史订单,请查看成本。但是存储密钥的开销加上使此可行所需的索引将超过将 'amount' 克隆到订单记录的存储成本。
总而言之 - 克隆成本数据会随时间变化。
事件溯源
这是 Event Sourcing 的一个很好的用例。 Martin Fowler 写了一篇很好的文章,建议你看看。
there are times when we don't just want to see where we are, we also want to know how we got there.
我们的想法是永远不要覆盖数据,而是为您想要保留历史记录的所有内容创建不可变的事务。在您的情况下,您将拥有 WaiterRelocationEvent
s 和 PriceChangeEvent
s。您可以通过按顺序应用每个事件来重新创建任何给定时间的状态。
如果您不使用 事件溯源,您将丢失信息。忘记历史信息通常是可以接受的,但有时则不然。
Lambda 架构
由于您不想对每个请求都重新计算所有内容,因此建议实施 Lambda Architecture。该架构通常用 BigData 技术和框架来解释,但您可以使用 Plain Old Java 和 CronJobs 来实现它。
它由三部分组成:批处理层、服务层和速度层。
批处理层 定期计算数据的聚合版本,例如,您将每天计算一次月收入。所以当月的收入每天晚上都会变化,直到一个月结束。
但是现在您想实时了解收入。因此,您添加了一个速度层,它将立即应用当前日期的所有事件。现在,如果收到当月收入请求,您将把 Batch Layer 和 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 的所有信息。
- 获取订单 31
- 获取产品21在2016-06-14的价格
- 获取日期之前的最后一个 PriceChangeEvent 或如果 none 存在则使用 FirstPrice
- 通过将检索到的价格乘以数量来计算总价
- 获取服务员 11
- 2016-06-14获取服务员餐厅
- 获取日期之前的最后一个 WaiterRelocationEvent 或如果 none 存在则使用 FirstRestaurant
- 通过检索服务员的餐厅 ID 获取餐厅名称
如您所见,它变得很复杂,因此您应该只保留有用数据的历史记录。
- 我不会在计算中涉及搬迁事件。它们可以存储,但我会直接在订单中存储餐厅 ID 和服务员 ID。
- 另一方面,价格历史记录可能很有趣,可以用来检查价格变动后订单是否下降。在这里,您可以使用 Lambda Architecure 来计算包含原始订单价格和价格历史记录的完整订单。
总结
- 决定要保留哪些数据的历史记录。
- 为该数据实施事件溯源。
- 使用 Lambda 架构 加速常用查询。