文档数据库如MongoDB并非无模式,而是将数据库设计与应用程序设计整合。开发者关注领域模型,通过嵌入、引用和模式验证维护数据完整性,实现敏捷开发。
译自:Rethinking Data Integrity: Why Domain-Driven Design Is Crucial
作者:Franck Pachot
开发者常常被不公正地指责对数据完整性漫不经心。逻辑是:没有SQL数据库的严格结构,开发者就会冲动编码,跳过正式设计,将其视为障碍,而非构建可靠系统的关键一步。
由于这种误解,许多数据库管理员(DBA)认为,保证数据质量的唯一方法是使用关系型数据库。他们认为,使用MongoDB这样的文档数据库意味着无法确保数据建模的正确性。
因此,DBA必须在任何应用程序持久化或共享数据之前,在其选定的数据库中预定义和部署模式。这也意味着应用程序的任何演进都需要DBA在新的发布版本到达用户之前验证并运行迁移脚本。
然而,开发者和DBA一样关心数据完整性。他们投入大量精力于应用程序的领域模型,并避免通过将其映射到不反映应用程序用例的规范化数据结构来削弱它。
不同数据库模型,不同数据模型
关系型数据库和文档数据库在数据建模上采取不同方法。
在文档数据库中,你仍然需要设计数据模型。变化在于设计发生的位置和方式,它与领域模型和应用程序的访问模式紧密对齐。这在实践领域驱动设计(DDD)的团队中尤其如此,开发者投入时间理解领域对象、关系和使用模式。
数据模型与开发过程同步演进——包括头脑风暴想法、原型设计、发布最小可行产品(MVP)以获取早期反馈,并迭代形成一个稳定、可投入生产的应用程序。
关系型建模通常始于在应用程序完全理解之前创建的规范化设计。这个模型必须随后服务于各种未来的工作负载和不可预测的数据分布。例如,一个为学术软件设计的数据库模式可以被小学和大型大学同时使用。这说明了关系型数据库的优势:暴露给应用程序的逻辑模型是相同的,即使工作负载差异很大。
相比之下,文档建模是为特定的应用程序使用量身定制的。MongoDB不是将领域模型转换为规范化表(这会增加抽象并隐藏性能优化),而是直接以它们在代码和业务逻辑中出现的方式存储聚合。文档反映业务事务,并以连续块的形式存储在磁盘上,使物理模型与领域模式对齐,并针对访问模式进行优化。
以下是这两种模型的其他比较方式。
文档建模处理关系
关系型数据库常被认为擅长处理数据间的“强关系”,但这部分是由于对名称的误解——relations 指的是数学上的元组(行)集合,而不是它们之间的连接,后者才是relationships。规范化实际上会削弱强关系,解耦实体,这些实体随后在查询时通过连接进行匹配。
在实体关系图(ERD)中,关系通过主键和外键实现,显示为简单的一对一或一对多链接。ERD不捕捉诸如导航方向或实体之间所有权之类的特征。多对多关系通过连接表建模,将其拆分为两个一对多关系。ERD中关系的唯一属性是区分一对一(直线)和一对多(鸟足),并且无论“多”是少数还是数十亿,数据模型都是相同的。
相比之下,面向对象设计中的统一建模语言(UML)类图更丰富:它们具有导航方向,并区分关联、聚合、组合和继承。在MongoDB中,这些概念自然地映射到:
- 组合(例如,一个订单及其订单项)通常表现为嵌入式文档,共享生命周期并防止部分删除。
- 聚合(一个客户及其订单)在生命周期不同或父级所有权共享时使用引用。
- 继承可以通过多态性表示,这是一个ERD不直接捕捉并用可空列来规避的概念。
面向对象应用程序中的领域模型和MongoDB文档更好地反映了真实世界的关系。在关系型数据库中,实体的模式是固定的,而关系在运行时通过连接解决——这更像数据科学家在分析期间发现关联。SQL的外键防止孤立行,但在编写SQL查询时并未明确引用它们。每个查询都可以定义一个不同的关系。
模式验证保护数据完整性
MongoDB是模式灵活的,而非无模式的。此功能对于早期项目——例如头脑风暴、原型设计或构建MVP——特别有价值,因为在写入数据之前不需要执行数据定义语言(DDL)语句。模式存在于应用程序代码中,文档按原样存储,最初无需额外验证,因为一致性由写入和读取它们的同一应用程序确保。
随着模型的成熟,你可以直接在数据库中定义模式验证规则——字段要求、数据类型和接受的范围。你不需要立即声明每个字段。你可以在模式成熟、稳定和共享时添加验证。这确保了当多个组件依赖相同字段时,或在索引时结构的一致性,因为只有应用程序使用的字段才有助于索引。
模式灵活性在应用程序的每个阶段都能提高开发速度。在原型设计的早期,你可以自由添加字段,而不必担心立即验证。随后,通过模式验证,你可以依赖数据库来强制执行数据完整性,减少编写和维护检查传入数据的代码的需求。
模式验证还可以强制执行物理边界。如果你将订单项嵌入到订单文档中,你可能会验证数组不超过某个阈值。MongoDB不会像SQL的检查约束那样直接失败(这通常会导致未处理的应用程序错误),而是可以记录警告,在不中断用户操作的情况下提醒团队。这使得应用程序在仍然标记潜在异常或必要演进的同时保持可用性。
应用程序逻辑 vs. 外键
在SQL数据库中,外键是约束,而不是关系本身的定义,关系在查询时进行评估。SQL连接通过将列列为过滤谓词来定义关系,并且JOIN子句中不使用外键。外键有助于防止某些由规范化引起的异常,例如孤立子行或级联删除。
MongoDB采取了不同的方法:通过嵌入紧密耦合的实体,可以预先解决主要的完整性问题。例如,将订单项嵌入其订单文档中,意味着孤立的订单项在设计上是不可能的。引用关系由应用程序逻辑处理,通常在将其值嵌入文档之前从稳定的集合(值列表)中读取。
由于MongoDB模型是为已知的访问模式和生命周期构建的,因此引用完整性是通过业务规则而非泛泛强制来维护的。在实践中,这更好地反映了真实世界的流程,其中更新或删除必须遵循特定条件(例如,降价可能适用于进行中的订单,但涨价可能不适用)。
在关系型数据库中,模式是与应用程序无关的,因此你必须防御任何可能的数据操作语言(DML)修改,而不仅仅是那些由有效业务事务引起的修改。在应用程序中这样做将需要额外的锁或更高的隔离级别,因此通常更高效的做法是声明外键由数据库强制执行。
然而,当领域用例得到充分理解时,仅需针对少数情况进行保护,并可将其集成到业务逻辑本身中。例如,一个产品在进行中的事务正在使用时永远不会被删除。业务工作流通常在产品实际删除之前很早就将其标记为不可用,并且事务的生命周期足够短,不会出现重叠,从而无需额外检查即可防止孤立数据。
在领域驱动模型中,模式围绕特定的应用程序用例设计,完整性可以由应用程序团队与业务规则一同完全管理。虽然额外的数据库验证可以作为一种保障,但它可能会限制可伸缩性,特别是在分片时,并限制灵活性。另一种方法是运行一个周期性聚合管道,异步检测异常。
下次你再听到那个误解时
MongoDB并非意味着“无设计”。它意味着将数据库设计与应用程序设计集成——通过嵌入、引用、模式验证和应用程序级别的完整性检查来反映实际的领域语义。
这种方法使数据建模成为开发者的首要关注点,直接与代码中领域对象的表示方式对齐。数据库结构与应用程序一同演进,并且完整性在交付应用程序本身的相同语言和管道中强制执行。
在DBA只关注数据库模型和SQL操作的环境中,外键可能看起来不可或缺。但在同一个团队处理数据库和应用程序的DevOps工作流中,模式规则可以首先在代码中实现,并在规范稳定后在数据库中进行精炼。这避免了维护两个独立模型以及相关的迁移开销,从而在保持完整性的同时实现更快速、迭代的发布。