2016 年 5 月

第 31 卷,第 5 期

领先技术 - 构建历史 CRUD

作者 Dino Esposito | 2016 年 5 月

Dino Esposito关系数据从上世纪 70 年代起就已使用,几代研发人员在开始和结束他们的职业生涯时并没有学习或稍微考虑一下数据存储的替代方法。最近,大型社交网络提供了关系数据库不能用于所有可能的业务方案的有力证据。当你遇到(真正的)大量无模式数据时,关系数据库可能有时候是瓶颈,而不是管道。

你能想象如何在一个包含数十亿记录的包罗万象的关系数据库中,立刻计算出对帖子发表了评论的朋友的喜好吗? 更不用说将帖子的定义限制到一个严格架构已经很有挑战性了。单就业务生存而言,社交网络在某个时期演变为将其存储焦点转移至关系和非关系数据商店的融合,从而正式开启了多语言数据的业务。

从社交网络软件体系结构中学到的重要一课是,就业务方面而言,有时候你拥有的数据普通存储并不是理想的方法。它在存储事件发生的细节以及该特定事件涉及的数据方面更胜一筹,而不是仅存储你已经获得的任何数据。

在这篇文章中,我将首先深挖事件源的业务基础 - 使用已登录事件作为应用程序的主要数据源 - 然后讨论如何根据事件刷新现有的创建、读取、更新和删除 (CRUD) 技术。首先需要弄清楚的是,问题不是你是否需要事件源,而是你什么时候需要它以及如何对它进行编码。

了解动态数据模型

使用多语言数据在今天很热门:结构化数据的关系数据库、更少结构化数据的 NoSQL 数据商店、首选项和日志的键值词典以及关系和相互关联的图形数据库。介绍并排运行的不同存储模型是一个方向正确的步骤,但是在我看来更像是对可见症状的有效方法,而不是对真实和深层问题的解决方案。

几十年来,关系模型在为读取和写入数据提供一系列平衡的优点方面非常有效。关系模型易于查询和更新,尽管它在一些(非常)极端的情况下存在局限性。在具有几百万条记录和几百列的表格上,整体性能会大大降低。此外,数据架构被固定,并且需要数据库结构知识来创建临时和快速查询。换句话说,你在现在的世界进行的编码,一个全面的模型,如大型前期关系模型,是一个庞大的约束,它首先限制你的表现形式,然后限制你的编程功能。最后,模型仅仅是模型,它不是你在真实世界里直接观察到的。在真实世界里,你看不到任何模型。而是你使用模型以封装一些广为理解的和可重复的行为。最后,在真实的世界里,你观察事件但是期望在一个受约束的(关系)模型中存储事件相关的信息。当这被证明难以操作时,深入了解仅发布一些架构和索引约束的替换存储模型。

基于事件的存储模型。

几十年来,仅保存实体的当前状态很有帮助且功能强大。当你保存一个给定的状态时,你覆盖了现有的的状态,因此丢失了所有以前的信息。这种行为本身不值得赞扬,也没必要责怪。多年以来,它被证明是一个有效的方法,并获得了广泛的认可。只有业务域和客户对丢失任何过往状态能否接受具有发言权。事实表明,多年来对于大部分企业,它是可以接受的。这个趋势正在改变,因为越来越多的业务应用程序要求跟踪业务实体的全部历史记录。多年来我们称之为 CRUD 的是指普通的创建、读取、更新和删除操作,并在普通关系表基础上建模,现在它正演变成我们通常所指的历史 CRUD。历史 CRUD 只是一个 CRUD 代码库,在这里实施会设法跟踪整个更改列表。

真实的世界充满了业务线 (LoB) 系统,它们以某种方式跟踪域中发生的事件。此类应用程序已经存在了几十年 - 有一些甚至是用 COBOL 或 Visual Basic 6 编写的。例如,毫无疑问,记账应用程序跟踪发票上所有可能发生的更改,例如日期或地址更改、贷记通知单发出等等。在一些业务方案中,从软件早期阶段跟踪事件已经是一个必备功能,并且通常归于审核功能的更广泛领域。

因此,审核业务事件在软件中并不是一个新的概念。几十年来,开发团队反复解决同样的问题,用他们可能找到的最好的方法重新设计和研究已知的技术。现在,广泛应用的审核业务事件有了一个更有吸引力的名字 - 事件源。

业务事件编码之路

所以让我们假定你有一个概念性的简单应用程序,比如让用户预定会议室等共享资源的应用程序。当用户查看给定的预定状态时,呈现在她面前的也许不仅是目前的预定状态,还有从创建后的整个更新列表。图 1 显示了视图一个可能的基于时间的 UI 。

整个预定历史的基于时间的视图。
图 1 整个预定历史的基于时间的视图

你如何设计一个预定数据模型,使其成为历史 CRUD,而不是基于状态的普通 CRUD? 添加更多列到表定义还不够。CRUD 和历史 CRUD 之间的关键区别在于,在历史 CRUD 中,你想存储同一实体的多个副本,在给定的时间它经历过的每个业务事件均有一个副本。图 2 显示关系数据库预定表的可能的新结构。

历史 CRUP 应用程序可能的关系数据模型
图 2 历史 CRUD 应用程序可能的关系数据模型

图 2 中描述的表格有一系列预期列,完全体现业务实体的状态,外加几个其他列。你至少需要一个主键列用于特别标识表格中的一行。接下来,你想有一个时间戳列指示数据库上的操作时间,或仅仅是对业务有意义的任何时间戳。一般来说,列用于将一个安全日期关联到实体状态。最后,你想有一列描述被记录的事件。

它仍是一个关系表,并且仍管理应用程序需要的预定列表。未添加新技术,但是从概念来说,图 2 中的架构化表格是来自经典 CRUD 的重大突破。添加记录到新表格很简单。你只需在收到系统里发生了一些事情,必须进行跟踪的通知时填写并附加记录。有关 CRUD 中的 C 就到这里;那么其他操作是怎样的?

在历史 CRUD 中进行更新和删除

一旦经典关系表被转变为基于事件的历史表,更新和删除的角色和相关性会显著更改。首先,更新消失。关于实体逻辑状态的任何更新现在作为一个新的记录实现,这个附加的新记录跟踪新的状态。

删除的问题更为棘手,关于如何对其进行编码的最终决定属于域业务逻辑的范围。在理想的基于事件的世界里,不存在删除。数据只是增加,因此删除只是新事件的增加,通知你实体逻辑上不再存在。但是,原则上并未禁止从表格物理删除数据,并且仍可发生。但是需要注意,在基于事件的方案中,要删除的实体不是由单一记录组成,而是包含记录集合,如图 2 中所示。如果你决定删除一个实体,那么你必须删除与之相关的所有事件(和记录)。

读取实体状态。

你从在你的应用程序中记录业务事件中得到的最大好处是,你从来不会丢失任何东西。你可以在任何给定的时间潜在的跟踪系统状态,确定导致给定状态的确切的操作顺序,并且可以完全或部分撤销那些事件。这为自助商业智能和业务分析中的假定方案奠定了基础。更准确地说,你将不会自动通过你的应用程序获得这些现成功能,但是你已经有了你需要开发的任何数据,例如除现有应用程序以外的扩展插件。

历史 CRUD 最困难的部分是读取数据。现在你正在示例预定系统中跟踪所有相关业务事件,但是没有地方可以让你轻松获得常设预定的完整列表。例如,没有快捷和轻松的方法知道下周你会有多少预定。这是预测最适合之处。图 3 总结了从普通的 CRUD 演变至历史 CRUD 的系统的整体结构。

历史 CRUD 系统的结构
图 3 历史 CRUD 系统的结构

基于事件的系统必然适合于在命令和查询堆栈之间实现巧妙分离。从表示层,用户触发在应用程序和域层进行操作的任务,涉及过程中的所有业务逻辑组件。命令是业务任务的触发器,会更改系统的目前状态,这意味着必须提交一些内容,从逻辑上修改现有状态。如前文所述,在基于事件的系统中 - 即使系统是普通的简单 CRUD 系统 - 更改状态意味着添加记录表明用户创建或更新了一个特定的预定。图 3 中标有“Event Repository”的程序块代表负责保持事件的任意代码层。根据具体技术,事件存储库程序块可以是基于实体框架的存储库类,也可以是来自文档数据库的包装器 ( Azure DocumentDB、RavenDB 或 MongoDB)。更有意思的是,它可以是使用事件存储 API 的包装器类,例如 EventStore 或 NEventStore。

在基于事件的结构中,给定实体的状态根据要求进行计算。此过程以事件重播的名义,包含查询所有与特定实体相关的事件,并将它们全部应用到实体类的崭新实例。在循环末尾,实体实例是更新的,因为它包含经历过所有记录事件的新实例的状态。

一般来说,事件日志处理会构建数据预测并从更低级别的数据量提取动态数据模型。这在图 3 中称为读取模型。除了相同的事件日志外,你可以构建提供各种前端的所有数据模型。要使用 SQL 类别,从已记录的事件构建数据预测与从关系表构建视图是相同的。

重播事件以确定实体的当前状态用于查询一般来说是一种可行的选择,但是它变得越来越低效,因为事件的数量和请求的频率会随着时间推移而增加。你不想每次只是为了查看银行账户的余额而浏览几千条记录。同样的,你不想在查看待预定列表时浏览几百个事件。为了解决这些问题,读取模型经常采用经典关系表的形式,与记录事件表在编程上保持同步。

总结

大部分应用程序仍可粗略的划分为 CRUD 应用。同样 Fackbook 可以用 CRUD 这样的方式表现,也许只是比平均数大一点点。严格地说,对于大部分用户,最后的已知正常状态仍足够,但是认为这个视图已经不够的客户数量正在增长。下一位客户也许正是你的最佳客户。本文仅涉及历史 CRUD 的皮毛。下个月我将提交具体的例子。请继续关注!


Dino Esposito*是《Microsoft .NET: 构建面向企业的应用程序》(Microsoft Press,2014 年)和《使用 ASP.NET 构建新型 Web 应用程序》(Microsoft Press,2016 年)的作者。Esposito 是 JetBrains 公司 .NET 和 Android 平台的技术推广专家,经常在全球性行业活动上发表演讲,他在 software2cents.wordpress.com 和 Twitter: @despos.*上分享了他的软件构想。

衷心感谢以下 Microsoft 技术专家对本文的审阅: Jon Arne Saeteras