领域驱动设计--领域驱动设计到数据建模实践(十)

----- 学习笔记 -----

过去,系统的软件设计是以数据库设计为核心,当需求确定下来以后,团队首先开始进行数据库设计。因为数据库是各个模块唯一的接口,当整个团队将数据库设计确定下来以后,就可以按照模块各自独立地进行开发了。在上面的过程中,为了提高团队开发速度,尽量让各个模块不要交互,从而达到各自独立开发的效果。但是,随着系统规模越来越大,业务逻辑越来越复杂,我们越来越难于保证各个模块独立不交互了。

在进入DDD的数据库设计落地之前,先了解DDD基于微服务的基本设计理念:

DDD基于微服务的概念理解

什么是DDD?

DDD不是一种架构风格,而是一种方法论。什么是方法论,每个人按照自己的想法来设计就是一套方法论;DDD是一种业务比较认可,对于微服务拆分的一种方法论。

为什么在微服务的大环境下DDD才流行?

  • 微服务区别于系统,服务是一组想对较小且独立功能单元,是用户感知最小功能集。

  • DDD计的模型中具有边界的最小原子是聚合,聚合和聚合之间由于只通过聚合根进行关联,所以当需要把一个聚合根从一个限界上下文移动到另外一个限界上下文的时候,非常低的移动成本可以很容易地对微服务进行重构。

  • 微服务和DDD的理念不谋而合,在微服务的大环境下DDD的流行是趋势。

为什么很多行业在微服务的架构上还是MVC架构?

  • 固化思维,从学校到工作,从学习到开源架构都是采取这种MVC架构;

  • 很多行业是基于传统行业进行项目改造,只是中间件用了微服务相关,但是业务没有明显的拆分依据,换汤不换药;

  • 资源成本,基于DDD的拆分成本比较高,主要体现在事件风暴、领域划分、实体聚合,上下文边界确定;

  • 对于企业来说,一个人能搞定的,不需要花大量的人力,时间去做规划;

  • 三层架构属于贫血模型,虽然不符合面向对象编程思想,架构结构比较简单,上手容易。

从技术上来讲DDD和MVC有什么区别?

MVC和DDD主要的区别在Service层,主要区别:

  • MVC:Service 实现全量业务逻辑,BO(Business Object)保存业务对象,属于贫血模型;

  • DDD:Service 实现少量业务逻辑,DO(Domain Object) 包含聚合跟、聚合、实体、值对象,从技术上来看就是实体,里面有相关领域的行为、动作、属性、依赖关系,属性充血模型。

DDD的基础概念

图片

  • 领域:领域是DDD中最大的概念,主要确定边界范围,领域又分为核心域(核心业务逻辑)、通用域(公共业务逻辑)、支撑域(基础第三方业务逻辑);

  • 界限上下文:在界限上下文中要建立通用语言,望文生义,就是大家都看得到,认可的叫法就是通用语言;界限上下文主要是来确定领域边界;

  • 聚合根:聚合根式一个根实体,里面包含了许多聚合,而且该聚合根有唯一标识,可以协调里面的每个聚合,外部访问只能根据聚合根的唯一标识进行访问里面的私有内容;

  • 聚合:聚合是一个整体,把所有的实体和值对象组织起来,做到一个聚合的作用,确保领域在执行逻辑的时候,确定数据一致性,从设计上避免数据不一致性;

  • 实体:整体概念的多个属性归集到的属性集合,有唯一标识id,实体是实实在在的业务对象,具有业务属性、行为和业务逻辑;

  • 值对象:若干个属性的集合,只有数据初始化操作和有限的不涉及修改数据的行为,不包含业务逻辑

⚠️DDD建模过程

  • 采用风暴事件,根据句业务行为,找过过程中发生行为的实体和值对象;

  • 在众多实体中选出合适的作为聚合根,挑选依据主要是否有独立的生命周期和全局唯一的ID,是否可以修改或创建其它对象;

  • 根据业务单一职责和高内聚原则,找出于聚合根关联的实体和值对象;

  • 在聚合内根据聚合根、实体、值对象,画出对象引用模型和依赖关系模型;

  • 多个聚合根据业务语义和上下文划分到一个上下文界面,也就是一个小的微服务。

怎么理解领域、子域、核心域、通用域和支撑域

DDD 的领域就是边界内要解决的业务问题域。

  • 我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。

  • 领域的核心思想就是将问题域逐级细分,来降低业务理解和系统实现的复杂度。通过领域细分,逐步缩小微服务需要解决的问题域,构建合适的领域模型,而领域模型映射成系统就是微服务了

  • 核心域、支撑域和通用域的主要目标是:通过领域划分,区分不同子域在公司内的不同功能属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,其关注度也会不一样。

怎么界定上下文

  • 在事件风暴过程中,通过团队交流达成共识的,能够简单、清晰、准确描述业务涵义和规则的语言就是通用语言

  • DDD 在战略设计上提出了“限界上下文”这个 概念,用来确定语义所在的领域边界。

  • 通用语言:团队内部能够清晰、准确的描述业务模型的语言就是通用语言

  • 限界上下文:为通用语言划定边界,并提供语义上下文。领域内所有界限上下文的模型构成了整个领域模型。理论上,某些条件下,限界上下文的划分也最终确定了微服务的边界。

什么是实体和值对象

  • 实体有唯一ID,实体是通过ID来区分不同实体的。即使实体的属性都发生了改变,只要它的ID还是原来的ID,实体还是那个实体。

  • 值对象是通过属性来区分不同值对象的,值对象只要改变了属性值,就已经不是原来的值对象了。

  • 在领域建模时,我们可以将部分对象设计为值对象,保留对象的业务涵义,同时又减少了实体的数量;

  • 在数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。

怎么理解聚合和聚合根

  • 领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。

  • 你可以这么理解,聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化

  • 判断一个实体是否是聚合根,你可以结合以下场景分析:

    是否有独立的生命周期?是否有全局唯一 ID? 是否可以创建或修改其它对象?是否有专门的模块来管这个实体?

总结:

  • 聚合的特点: 高内聚、低耦合,它是领域模型中最底层的边界,可以作为拆分微服务的最小单位,但不建议你对微服务过度拆分。在对性能有极致要求的场景中,聚合可以独立作为一个微服务,以满足版本的高频发布和极致的弹性伸缩能力。

    一个微服务可以包含多个聚合,聚合之间的边界是微服务内天然的逻辑边界。有了这个逻辑边界,在微服务架构演进时就可以以聚合为单位进行拆分和组合了,微服务的架构演进也就不再是一件难事了。

  • 聚合根的特点: 聚合根是实体,有实体的特点,具有全局唯一标识,有独立的生命周期。一个聚合只有一个聚合根,聚合根在聚合内对实体和值对象采用直接对象引用的方式进行组织 和协调,聚合根与聚合根之间通过 ID 关联的方式实现聚合之间的协同。

  • 实体的特点: 有 ID 标识,通过 ID 判断相等性,ID 在聚合内唯一即可。状态可变,它依附于聚合根,其生命周期由聚合根管理。实体一般会持久化,但与数据库持久化对象不一定是 一对一的关系。实体可以引用聚合内的聚合根、实体和值对象。

  • 值对象的特点:无 ID,不可变,无生命周期,用完即扔。值对象之间通过属性值判断相等性。它的核心本质是值,是一组概念完整的属性组成的集合,用于描述实体的状态和特征。值对象尽量只引用值对象。

产生聚合的过程一般是,事件风暴->领域对象 ->实体、值对象->聚合根->聚合

DDD建模示例

领域驱动设计--领域驱动设计到数据建模实践(十)_第1张图片

了解DDD的设计理念和基本原则:

DDD(领域驱动设计)是对软件所涉及到的领域进行建模,以应对系统规模过大时引起的软件复杂性的问题。而在微服务微应用蔚然成风的今天,他们共同体现的分而治之的思想,使得领域建模显得尤为重要。

一、简析DDD


1. DDD溯源


在早期开发中,领域模型就是数据库设计,但是传统的开发存在一定问题。
(1)Service层很重,所有逻辑处理基本都放在service层。


(2)POJO作为 Service 层非常重要的一个实体,因为不同场景的需求做不同的变化和组合,造成 POJO 的几种不同模型(失血、贫血、充血)。


(3)随着业务变得复杂以后,包括数据结构的变化,各个模块就需要进行修改,原本清晰的系统经过不断的演化变得复杂、冗余、耦合度高,后果非常严重。

2. DDD介绍


(1)什么是领域(Domain)?
任何一个系统都会属于某个特定的领域,同一个领域的系统都具有相同的核心业务,因为他们要解决的问题的本质是类似的。因此,我们可以推断出,一个领域本质上可以理解为一个问题域,只要是同一个领域,那问题域就相同。所以,只要我们确定了系统所属的领域,那这个系统的核心业务,即要解决的关键问题、问题的范围边界就基本确定了。


(2)什么是驱动(Driven)?
驱动是领域驱动领域模型设计,是领域模型驱动代码的实现。


(3)什么是设计(Design)?
DDD中的设计主要指领域模型的设计。DDD是一种基于模型驱动开发的思想,强调领域模型是整个系统的核心价值。每一个领域,都有对应的领域模型,领域模型能够很好的帮我们解决复杂的业务问题。从领域和代码实现的角度来理解,领域模型绑定了领域和代码实现,确保了最终的代码实现是解决了领域中的核心问题。

3. PO、BO、VO


DDD中有三个对象需要理解,分别是PO、BO、VO。
(1)PO :persistent object持久化对象
有时也被称为Data对象,对应数据库中的entity;PO中不应该包含任何对数据库的操作;PO的属性是跟数据库表的字段对应的;PO对象需要实现序列化接口。
(2)VO :value object 值对象 / view object 表现层(视图)对象
value object 值对象:通常用于业务层之间的数据传递,和PO一样仅仅包含数据。但应是抽象出的业务对象,根据业务需要选择是否与表对应;
VO(view object)表现层对象,视图对象:用一个VO对象对应整个界面的值,主要对应页面显示的数据对象,根据业务需要选择是否与表对应。
(3)BO :business object 业务对象

主要作用是把业务逻辑封装为一个对象。这个对象可以包括一个或多个其它的对象。

4. 模式优势


(1)DDD能让我们知道如何抽象出限界上下文以及如何去分而治之。
分而治之 : 把复杂的软件拆分成若干个子模块,每一个模块都能独立运行和解决相关问题,并且分割后各个部分可以组装成为一个整体。抽象 : 使用抽象能够精简问题空间,而且问题越小越容易理解,比如说我们要对接支付,抽象的纬度应该是支付,而不是具体的微信支付还是支付宝支付。
(2)DDD的限界上下文可以完美匹配微服务的要求。
在系统复杂之后,我们需要用分治来拆解问题。一般有两种方式,技术维度和业务维度。技术维度类似 MVC模式等,业务维度则是指按业务领域来划分系统。微服务架构更强调从业务维度去做分治来应对系统复杂度, 而DDD也同样的着重业务视角。

二、DDD落地


1. 落地方法论

领域驱动设计--领域驱动设计到数据建模实践(十)_第2张图片

DDD实现的核心就是建立正确的领域模型,而领域模型的设计是一个循环优化的过程。其中划分边界上下文可以从梳理领域概念、梳理业务规则、梳理业务场景、梳理业务流程四个方面入手。远光远光天鸿基于DDD的思想提供领域建模指引,助力用户更轻松的构建领域模型。

2. 契合DDD思想


基于DDD思想实现的方案是一套完整而系统的设计方案,它能带给你从战略设计到战术设计的专家化、智能化的架构设计和规范指导。
(1)领域驱动设计是针对复杂系统设计的软件工程方法,它在战略层面有三个重要的点,一是聚焦业务核心价值,二是统一语言,三是业务领域划分。
(2)领域驱动设计思想与微服务设计思想契合,其中的领域、子域、核心域、通用子域、限界上下文等思想提供如何把握粒度拆分微服务。
(3)统一在云服务上管控精炼领域模型的过程,最终实现领域模型云端数据化。
(4)核心领域标准化,个性场景定制化。通过配置实现个性化定制,快速响应需求变更。

3. 实现DDD功能


(1)领域拆分:天生面向微服务设计,提供领域,子域,支撑组件,通用领域,限界上下文的建模。
(2)统一语言:提供了一套完整而系统的统一语言和设计方法,通过协同沟通规范了一体化设计。使得项目相关角色人员(需求、设计、开发等)可以在云服务使用相同元素进行设计,并且上一环节可以顺滑的传递过渡到下一环节。
(3)精炼领域模型:提供了领域模型的设计,模型与代码实现的绑定,模型即实现。可以通过模型的“正确”验证软件的“正确”,从而确保软件实现符合需求,提升软件质量,体现业务价值。
(4)场景设计:核心领域模型标准化后构建成的产品,在客户化实施过程中需要定制,可以通过二次开发,或者配置场景的方式调用中心层领域模型,快速响应定制。

4. 提炼领域模型


模型库内置标准化、专业化的领域模型、行业标准模型等多种模型,以及标准元素,提供丰富的模型资源,以及多种灵活快速的模型引用方式,协助用户快速构建各种业务模型。

DDD基于中台+微服务的业务建模落地

中台本质是业务模型,微服务是业务模型的系统落地,DDD 是一种设计思想,它可以同时指导中台业务建模和微服务设计,它们之间就是这样的一个铁三角关系。

DDD 强调领域模型和微服务设计的一体性,先有领域模型然后才有微服务,而不是脱离领域模型来谈微服务设计

领域驱动设计--领域驱动设计到数据建模实践(十)_第3张图片


下面利用以上DDD设计理念,看如何落地到数据库设计,包括关系型和NO-SQL的落地实践。

领域驱动设计--领域驱动设计到数据建模实践(十)_第4张图片

随着软件业的不断发展,软件系统变得越来越复杂,各个模块间的交互也越来越频繁,这时,原有的设计过程已经不能满足我们的需要了。因为如果要先进行数据库设计,但数据库设计只能描述数据结构,而不能描述系统对这些数据结构的处理。因此,在第一次对整个系统的梳理过程中,只能梳理系统的所有数据结构,形成数据库设计;接着,又要再次梳理整个系统,分析系统对这些数据结构的处理过程,形成程序设计。为什么不能一次性地把整个系统的设计梳理到位呢?

领域驱动设计--领域驱动设计到数据建模实践(十)_第5张图片

现如今,我们已经按照面向对象的软件设计过程来分析设计系统了。当开始需求分析时,

  • 首先进行用例模型的设计,分析整个系统要实现哪些功能;

  • 接着进行领域模型的设计,分析系统的业务实体。

在领域模型分析中,采用类图的形式,每个类可以通过它的属性来表述数据结构,又可以通过添加方法来描述对这个数据结构的处理。

因此,在领域模型的设计过程中,既完成了对数据结构的梳理,又确定了系统对这些数据结构的处理,这样就把两项工作一次性地完成了。

在这个设计过程中,其核心是领域模型的设计。以领域模型作为核心,可以指导系统的数据库设计与程序设计,此时,数据库设计就弱化为了领域对象持久化设计的一种实现方式。

领域对象持久化的思想

什么叫领域对象的持久化呢?在当今软件架构设计的主流思想中,面向对象设计成了主流思想,在整个系统运行的过程中,所有的数据都是以领域对象的形式存在的。譬如:

  • 要插入一条记录就是创建一个领域对象;

  • 要更新一条记录就是根据 key 值去修改相应的领域对象;

  • 删除数据则是摧毁这个领域对象。

假如我们的服务器是一台超级强大的服务器,那实际上不需要任何数据库,直接操作这些领域对象就可以了,但在现实世界中没有那么强大的服务器。因此,必须将暂时不用的领域对象持久化存储到磁盘中,而数据库只是这种持久化存储的一种实现方式。

按照这种设计思想,我们将暂时不使用的领域对象从内存中持久化存储到磁盘中。当日后需要再次使用这个领域对象时,根据 key 值到数据库查找到这条记录,然后将其恢复成领域对象,应用程序就可以继续使用它了,这就是领域对象持久化存储的设计思想。

所以,今天的数据库设计,实际上就是将领域对象的设计按照某种对应关系,转换成数据库的设计。同时,随着整个产业的大数据转型,今后的数据库设计思想也将发生巨大的转变,有可能数据库就不一定是关系型数据库了,也许是 NoSQL 数据库或者大数据平台。数据库的设计也不一定遵循 3NF(第三范式)了,可能会增加更多的冗余,甚至是宽表。

数据库设计在发生剧烈的变化,但唯一不变的是领域对象。这样,当系统在大数据转型时,可以保证业务代码不变,变化的是数据访问层(DAO)。这将使得日后大数据转型的成本更低,让我们更快地跟上技术快速发展的脚步。

领域模型的设计

此外,这里有个有趣的问题值得探讨:领域模型的设计到底是谁的职责,是需求分析人员还是设计开发人员?它是两个角色相互协作的产物。而未来敏捷开发的组织形成,团队将更加扁平化。过去是需求分析人员做需求分析,然后交给设计人员设计开发,这种方式就使得软件设计质量低下而结构臃肿。未来“大前端”的思想将支持更多设计开发人员直接参与需求分析,实现从需求分析到设计开发的一体化组织形式。这样,领域模型就成为了设计开发人员快速理解需求的利器。

总之,**DDD 的数据库设计实际上已经变成了:以领域模型为核心,如何将领域模型转换成数据库设计的过程。**

那么怎样进行转换呢?在领域模型中是一个一个的类,而在数据库设计中是一个一个的表,因此就是将类转换成表的过程。

领域驱动设计--领域驱动设计到数据建模实践(十)_第6张图片

上图是一个绩效考核系统的领域模型图,该绩效考核系统首先进行自动考核,发现一批过错,然后再给一个机会,让过错责任人对自己的过错进行申辩。这时,过错责任人可以填写一张申辩申请单,在申辩申请单中有多个明细,每个明细对应一个过错行为,每个过错行为都对应了一个过错类型,这样就形成了一个领域模型。

接着,要将这个领域模型转换成数据库设计,怎么做呢?很显然,领域模型中的一个类可以转换成数据库中的一个表,类中的属性可以转换成表中的字段。但这里的关键是如何处理类与类之间的关系,如何转换成表与表之间的关系。这时候,就有 5 种类型的关系需要转换,即传统的 4 种关系 + 继承关系。

传统的 4 种关系

传统的关系包含一对一、多对一、一对多、多对多这 4 种,它们既存在于类与类之间,又存在于表与表之间,所以可以直接进行转换。

1. 一对一关系

在以上案例中,“申辩申请单明细”与“过错行为”就是一对“一对一”关系。在该关系中,一个“申辩申请单明细”必须要对应一个“过错行为”,没有一个“过错行为”的对应就不能成为一个“申辩申请单明细”。这种约束在数据库设计时,可以通过外键来实现。但是,一对一关系还有另外一个约束,那就是一个“过错行为”最多只能有一个“申辩申请单明细”与之对应。

也就是说,一个“过错行为”可以没有“申辩申请单明细”与之对应,但如果有,最多只能有一个“申辩申请单明细”与之对应,这个约束暗含的是一种唯一性的约束。因此,将过错行为表中的主键,作为申辩申请单明细表的外键,并将该字段升级为申辩申请单明细表的主键。

领域驱动设计--领域驱动设计到数据建模实践(十)_第7张图片

2. 多对一关系

是日常的分析设计中最常见的一种关系。在以上案例中,一个过错行为对应一个税务人员、一个纳税人与一个过错类型;同时,一个税务人员,或纳税人,或过错类型,都可以对应多个过错行为。它们就形成了“多对一”关系。在数据库设计时,通过外键就可以建立这种“多对一”关系。因此,我们进行了如下数据库的设计:

领域驱动设计--领域驱动设计到数据建模实践(十)_第8张图片

多对一关系在数据库设计上比较简单,然而落实到程序设计时,需要好好探讨一下。比如,以上案例,在按照这样的方式设计以后,在查询时往往需要在查询过错行为的同时,显示它们对应的税务人员、纳税人与过错类型。这时,以往的设计是增加一个 join 语句。然而,这样的设计在随着数据量不断增大时,查询性能将受到极大的影响。

也就是说,join 操作往往是关系型数据库在面对大数据时最大的瓶颈之一。因此,一个更好的方案就是先查询过错行为表,分页,然后再补填当前页的其他关联信息。这时,就需要在“过错行为”这个值对象中通过属性变量,增加对税务人员、纳税人与过错类型等信息的引用。

3. 一对多关系

该关系往往表达的是一种主-子表的关系。譬如,以上案例中的“申辩申请单”与“申辩申请单明细”就是一对“一对多”关系。除此之外,订单与订单明细、表单与表单明细,都是一对多关系。一对多关系在数据库设计上比较简单,就是在子表中增加一个外键去引用主表中的主键。比如本案例中,申辩申请单明细表通过一个外键去引用申辩申请单表中的主键,如下图所示。

领域驱动设计--领域驱动设计到数据建模实践(十)_第9张图片

除此之外,在程序的值对象设计时,主对象中也应当有一个集合的属性变量去引用子对象。如本例中,在“申辩申请单”值对象中有一个集合属性去引用“申辩申请单明细”。这样,当通过申辩申请单号查找到某个申辩申请单时,同时就可以获得它的所有申辩申请单明细,如下代码所示:

public class Sbsqd {    private Set sbsqdMxes;    public void setSbsqdMxes(Set sbsqdMxes){          this.sbsqdMxes = sbsqdMxes;    }    public Set getSbsqdMxes(){          return this.sbsqdMxes;    }    ……}

4. 多对多关系

比较典型的例子就是“用户角色”与“功能权限”。一个“用户角色”可以申请多个“功能权限”;而一个“功能权限”又可以分配给多个“用户角色”使用,这样就形成了一个“多对多”关系。这种多对多关系在对象设计时,可以通过一个“功能-角色关联类”来详细描述。因此,在数据库设计时就可以添加一个“角色功能关联表”,而该表的主键就是关系双方的主键进行的组合,形成的联合主键,如下图所示:

领域驱动设计--领域驱动设计到数据建模实践(十)_第10张图片

以上是领域模型和数据库都有的 4 种关系。

因此,在数据库设计时,直接将相应的关系转换成数据库设计就可以了。同时,在数据库设计时还要将它们进一步细化。如在领域模型中,不论对象还是属性,在命名时都采用中文,这样有利于沟通与理解。但到了数据库设计时,就要将它们细化为英文命名,或者汉语拼音首字母,同时还要确定它们的字段类型与是否为空等其他属性。

继承关系的 3 种设计

第 5 种关系就不太一样了:继承关系是在领域模型设计中有,但在数据库设计中却没有。如何将领域模型中的继承关系转换成数据库设计呢?有 3 种方案可以选择。

1. 继承关系的第一种方案

首先,看看以上案例。“执法行为”通过继承分为“正确行为”和“过错行为”。如果这种继承关系的子类不多(一般就 2 ~ 3 个),并且每个子类的个性化字段也不多(3 个以内)的话,则可以使用一个表来记录整个继承关系。在这个表的中间有一个标识字段,标识表中的每条记录到底是哪个子类,这个字段的前面部分罗列的是父类的字段,后面依次罗列各个子类的个性化字段。

领域驱动设计--领域驱动设计到数据建模实践(十)_第11张图片

采用这个方案的优点是简单,整个继承关系的数据全部都保存在这个表里。但是,它会造成“表稀疏”。在该案例中,如果是一条“正确行为”的记录,则字段“过错类型”与“扣分”永远为空;如果是一条“过错行为”的记录,则字段“加分”永远为空。假如这个继承关系中各子类的个性化字段很多,就会造成该表中出现大量字段为空,称为“表稀疏”。在关系型数据库中,为空的字段是要占用空间的。因此,这种“表稀疏”既会浪费大量存储空间,又会影响查询速度,是需要极力避免的。所以,当子类比较多,或者子类个性化字段多的情况是不适合该方案(第一种方案)的。

2. 继承关系的第二种方案

如果执法行为按照考核指标的类型进行继承,分为“考核指标1”“考核指标2”“考核指标3”……如下图所示:

领域驱动设计--领域驱动设计到数据建模实践(十)_第12张图片

并且每个子类都有很多的个性化字段,则采用前面那个方案就不合适了。这时,用另外两个方案进行数据库设计。其中一个方案是将每个子类都对应到一个表,有几个子类就有几个表,这些表共用一个主键,即这几个表的主键生成器是一个,某个主键值只能存在于某一个表中,不能存在于多个表中。每个表的前面是父类的字段,后面罗列各个子类的字段,如下图所示:

领域驱动设计--领域驱动设计到数据建模实践(十)_第13张图片

如果业务需求是在前端查询时,每次只能查询某一个指标,那么采用这种方案就能将每次查询落到某一个表中,方案就最合适。但如果业务需求是要查询某个过错责任人涉及的所有指标,则采用这种方案就必须要在所有的表中进行扫描,那么查询效率就比较低,并不适用。

3. 继承关系的第三种方案

如果业务需求是要查询某个过错责任人涉及的所有指标,则更适合采用以下方案,将父类做成一个表,各个子类分别对应各自的表(如图所示)。这样,当需要查询某个过错责任人涉及的所有指标时,只需要查询父类的表就可以了。如果要查看某条记录的详细信息,再根据主键与类型字段,查询相应子类的个性化字段。这样,这种方案就可以完美实现该业务需求。

领域驱动设计--领域驱动设计到数据建模实践(十)_第14张图片

综上所述,将领域模型中的继承关系转换成数据库设计有 3 种方案,并且每个方案都有各自的优缺点。因此,需要根据业务场景的特点与需求去评估,选择哪个方案更适用。

NoSQL 数据库的设计

前面我们讲的数据库设计,还是基于传统的关系型数据库、基于第三范式的数据库设计。但是,随着互联网高并发与分布式技术的发展,另一种全新的数据库类型孕育而生,那就是NoSQL 数据库。正是由于互联网应用带来的高并发压力,采用关系型数据库进行集中式部署不能满足这种高并发的压力,才使得分布式 NoSQL 数据库得到快速发展。

也正因为如此,NoSQL 数据库与关系型数据库的设计套路是完全不同的。关系型数据库的设计是遵循第三范式进行的,它使得数据库能够大幅度降低冗余,但又从另一个角度使得数据库查询需要频繁使用 join 操作,在高并发场景下性能低下。

所以,NoSQL 数据库的设计思想就是尽量干掉 join 操作,即将需要 join 的查询在写入数据库表前先进行 join 操作,然后直接写到一张单表中进行分布式存储,这张表称为“宽表”。这样,在面对海量数据进行查询时,就不需要再进行 join 操作,直接在这个单表中查询。同时,因为 NoSQL 数据库自身的特点,使得它在存储为空的字段时不占用空间,不担心“表稀疏”,不影响查询性能。

因此,NoSQL 数据库在设计时的套路就是,尽量在单表中存储更多的字段,只要避免数据查询中的 join 操作,即使出现大量为空的字段也无所谓了。

领域驱动设计--领域驱动设计到数据建模实践(十)_第15张图片

增值税发票票样图

正因为 NoSQL 数据库在设计上有以上特点,因此将领域模型转换成 NoSQL 数据库时,设计就完全不一样了。比如,这样一张增值税发票,如上图所示,在数据库设计时就需要分为发票信息表、发票明细表与纳税人表,而在查询时需要进行 4 次 join 才能完成查询。但在 NoSQL 数据库设计时,将其设计成这样一张表:

{ _id: ObjectId(7df78ad8902c)  fpdm: '3700134140', fphm: '02309723‘,   kprq: '2016-1-25 9:22:45',  je: 70451.28, se: 11976.72,   gfnsr: {     nsrsbh: '370112582247803',     nsrmc:'xxxxx公司',…  },  xfnsr: {     nsrsbh: '370112575587500',     nsrmc:'xxxxx公司',…  },  spmx: [     { qdbz:'00', wp_mc:'蓝牙耳机 车语者S1 蓝牙耳机', sl:2, dj:68.37,… },     { qdbz:'00', wp_mc:'车载充电器 新在线', sl:1, dj:11.11,… },     { qdbz:'00', wp_mc:'保护壳 非尼膜属 iPhone6 电镀壳', sl:1, dj:24,…  }  ]}

在该案例中,

对于“一对一”和“多对一”关系,在发票信息表中通过一个类型为“对象”的字段来存储,比如“购方纳税人(gfnsr)”与“销方纳税人(xfnsr)”字段。

对于“一对多”和“多对多”关系,通过一个类型为“对象数组”的字段来存储,如“商品明细(spmx)”字段。

在这样一个发票信息表中就可以完成对所有发票的查询,无须再进行任何 join 操作。

同样,采用 NoSQL 数据库怎样实现继承关系的设计呢?由于 NoSQL 数据库自身的特点决定了不用担心“表稀疏”,同时要避免 join 操作,所以比较适合采用第一个方案,即将整个继承关系放到同一张表中进行设计。这时,NoSQL 数据库的每一条记录可以有不一定完全相同的字段,可以设计成这样:

{ _id: ObjectId(79878ad8902c),  name: ‘Jack’,  type: ‘parent’,  partner: ‘Elizabeth’,  children: [    { name: ‘Tom’, gender: ‘male’ },    { name: ‘Mary’, gender: ‘female’}  ]},{ _id: ObjectId(79878ad8903d),  name: ‘Bob’,  type: ‘kid’,  mother: ‘Anna’,  father: ‘David’}

以上案例是一个用户档案表,有两条记录:Jack 与 Bob。但是,Jack 的类型是“家长”,因此其个性化字段是“伴侣”与“孩子”;而 Bob 的类型是“孩子”,因此他的个性化字段是“父亲”与“母亲”。显然,在 NoSQL 数据库设计时就会变得更加灵活。

总结

将领域模型落地到系统设计包含 2 部分内容,本节演练了第一部分内容——从 DDD 落实到数据库设计的整个过程:

  • 传统的 4 种关系可以直接转换;

  • 继承关系有 3 种设计方案;

  • 转换成 NoSQL 数据库则是完全不同的思路。

有了 DDD 的指导,可以帮助我们理清数据间的关系,以及对数据的操作。不仅如此,在未来面对大数据转型时更加从容。

【问题+回复】

1

没太理解领域对象概念,数据库里每一条数据都是一个领域对象,那么,这是什么领域,因为,有的表存的实体,有的表存的关系,有的表存的明细数据

回复:

领域就是不同行业的业务领域知识,比如财务有财务领域,税务有税务领域,金融有金融领域。DDD强调,开始系统设计前先理解业务领域知识

2

领域对象、值对象、实体、聚合、聚合根的关系,能用商品的例子解释下吗?

回复:

建模过程中的都有对象都是领域对象,其中订单是实体、商品是值对象、订单与订单明细是聚合,其中订单是聚合根。

3

主键,即这几个表的主键生成器是一个,某个主键值只能存在于某一个表中,不能存在于多个表中。继承关系中第二种方案 。

这几个表的主键都是按照一样的算法生成的?即主键Id一样的嘛,如果一样的查某一个指标怎么知道那张表了?

回复:

比如在文中的案例中,前端查询时,指标类型必填,这时指标类型与表会建立一个对应关系

4

继承关系的第二种和第三种,感觉不是一样的吗?都是多个子表和一张主表?

回复:

第二种只有子表没有主表

你可能感兴趣的:(领域驱动设计,领域驱动设计)