习惯是强大的,但往往是无形的。往往在不经意间,不知不觉中掉进了习惯的陷阱。在我们的项目中,为了能够保存分析报告和用户设置的报告查询条件,我们将这些信息作为报告元数据存储在MongoDB中。需要存储的元数据包括:报表类别(ReportCategory)报表(Report)报表查询条件(QueryCondition)一个报表类别包含多个报表,一个报表只能属于一个类别。每个报表提供多种标准查询条件和多种用户自定义查询条件。我需要为这些元数据设计MongoDB的数据库模式。最初考虑将这三个概念一起定义为元数据表中的一条记录。后来想到对于一个报表来说,需要经常对报表的查询条件进行增删改查,貌似应该把查询条件单独分开。报告分类和报告呢?分开报告是否合适?对于MongoDB这样的Document数据库,使用Report作为ReportCategory的内嵌属性也是可行的,至少不会像关系型数据库那样产生数据冗余。如果要分开,当你需要查询某个类别下的所有报表时,你就不得不冗余地做一个链接。好纠结!似乎任何设计都是可行的,但似乎总有不尽如人意的地方。边想边突然想到,对于这种面向文档的NoSQL数据库,用Aggregate来观察表记录会比较合适。这个想法仿佛闪电般迅捷犀利,猛地冲进了我脑海中的思绪,一下子点燃了我的设计思维。这里所谓的“聚合”并不是面向对象中表达对象关系的概念,而是领域驱动设计(DDD)对对象边界的思考。关于Aggregate的设计,我根据以往的经验整理出了五个设计原则:Aggregation作为边界,主要用于维护业务的完整性。此时,业务规则中定义的不变量(Invariant)应该作为聚合遵循,如果边界内的非聚合根实体对象可能被其他调用者单独调用,则应该作为一个单独的聚合分离出来。聚合边界内的非聚合根对象应该与聚合根有直接或间接的引用关系,可以通过对象引用;如果必须用Id引用,说明引用的对象不属于聚合。如果一个对象不能在没有另一个对象作为其主要对象的情况下存在,则该对象必须属于主要对象如果聚合边界内的实体对象可能被多个聚合引用,则应首先将实体对象视为单独的聚合。这些设计原则是我在探索聚合设计时的一些想法。经过多次实践,我觉得还是比较有指导价值的。这里就不展开了,在以后的文章中会详细介绍。就说这个例子,我们怎么用这些原则来思考ReportCategory、Report和QueryCondition之间的关系呢?显然,应用这些原理,我觉得之前纠结混乱的思路就可以迎刃而解了。从业务完整性的角度来看,Report虽然属于ReportCategory,但是两者并没有很强的约束关系,即不存在业务不变性(Invariant)。比如ReportCategory可以是没有Report的空分类,或者我们可以不带ReportCategory独立查询所有的Reports。如果我们将Report放在ReportCategory聚合中,由于Report可能会被单独调用,聚合的边界保护反而成为障碍,这是不合理的。所以,我们可以得出第一个结论:ReportCategory和Report应该属于两个不同的聚合。基于第四条原则,我们可以提出这样一个问题:当QueryCondition缺少Report对象时,它还存在吗?答案一目了然,没有Report,就没有QueryCondition。没有什么可以隐藏的了!第二个结论自然而然:Report和QueryCondition应该属于同一个聚合。于是,模型呼之欲出了:上图是领域模型而不是数据模型。从领域驱动设计的角度来看,这才是正确的打开姿势。那么,用这种领域模型来指导MongoDB的Schema设计,是否有将领域混入技术实现之嫌?从设计的角度来看,正确的解决方案是首先考虑领域模型,DB的技术实现设计应该满足领域模型。只有当领域模型可能阻碍技术实现,或者根据领域模型得到的Schema设计不满足性能或其他质量属性要求时,才需要对领域模型进行逆向调整。对于MongoDB这样的面向文档的数据库,使用聚合概念来指导Schema设计是水到渠成的事。不仅不会让人觉得不一致,反而让Repository的实现更加简单自然。在项目开发过程中,先入为主地进行技术选型,习惯性的开始MongoDB的Schema设计,却忘记了领域驱动设计的指导原则。技术人员往往对技术实现很兴奋,所以他们忽略了领域设计的驱动力,所以要小心!【本文为专栏作家“张艺”原创稿件,转载请联系原作者】点此阅读更多该作者好文
