数据库schema不是CRUD服务的一切

谨以本文向我脑海中那些不成熟的想法致敬。javascript

序言

受疫情影响呆在家中的这段时间里,我收尾了《Clean Architecture》。这本书给了我许多新知识和启发,包括本文的中心论点——数据库schema不是CRUD服务的一切,也是在读书过程当中想到的。在书中,做者的原话是java

But the database is not the data model

它出如今书中第六部分《Details》的第一个章节中。做者认为,从架构的角度来看,数据库不是一个实体而是一个细节,不足以成为架构中的一个元素。他甚至打了个比方:数据库对于架构而言,就好像门把手对于房子通常。而且,做者进一步澄清了他的观点:他口中所说的数据库,不是指的数据模型。应用内的数据结构对架构而言相当重要,但数据库并非数据模型。node

这不由让我回忆起了本身早期写设计文档的套路。git

在个人从业生涯早期(说得好像我从业好久了同样),每当须要开发一个新的Web服务时,必须先写一份简要的设计文档,向上级清楚地表达个人实现思路,包括:程序员

  1. 如何与其它服务协做完成产品提出的需求;
  2. 服务的接口描述;
  3. 数据的存储结构;
  4. 关键的算法等。

那时候的我会先考虑数据的存储结构,而后定义接口,最后才是与其它服务的协做。这些早期设计文档的其中一个特色是:接口的响应格式,与数据的存储结构是相同的。github

比方说我要设计一个网上商城的订单服务,可能会提供以下查询特定订单的接口算法

GET /order/:id

其响应格式可能以下mongodb

{
  order: {
    id: 'F122663A-A5DC-451A-9B79-92DCE2EE41F1',
    price: '100.00',
    products: [
      {
        id: 1,
        name: 'MacBook Pro',
        price: '19999.00'
      },
      {
        id: 2,
        name: 'iPhone',
        price: '6999.00'
      }
    ]
  }
}

为了保存这种“不平坦”的对象,将会用MongoDB做为存储——除了文档的主键_id以外,其它字段在接口和存储之间一一对应。数据库

不只仅是响应格式,在这个Web服务内所操做的也是一样结构的对象。用MongoDB Node.JS Driver得到的订单刚好是一个JS对象,它与collection的文档有着如出一辙的结构。以后这些对象会在代码内处处流通,不加修饰地使用。定义了数据库schema(就算是用MongoDB也有一套脑内的schema)后,其它的一切也就跟着肯定了。数组

业界甚至有工具能够直接从数据库获得API,好比postgrest

不少时候,数据库schema成了一个应用内事实上的数据模型。可是,即使它们能够偶然同样,也不要认为它们总应该同样。

适合存储,不必定适合计算

以MySQL为例,在CREATE TABLE语句中某一列的类型实际上决定的是存储时分配的空间的多少。但适合存储的类型,并不必定也适合业务逻辑的运算。

好比说,要在MySQL中存储“开关型”的数据,即诸如“是否启用”或“是否已支付”这样非此即彼的状态时,一般定义为TINYINT类型,用0表示逻辑假(“未启用”和“未支付”),1表示逻辑真(“已启用”和“已支付”)。但对代码而言,比起用数值类型,布尔类型才是更恰当的选择。尤为是当所选择的语言并无将0与false、1与true等价起来的时候——在Common Lisp中,(if 0 1 2)的求值结果为1。

适合计算,不必定适合存储

一般数据结构在内存中比在磁盘上要容易表达得多,因此代码中使用的数据结构会比数据库中存储的要灵活很多,这一样形成了二者的不匹配。

以我本身开发的提醒工具cuckoo为例,应用内有两种对象:任务和提醒。任务描述了要作的事情,提醒描述了在何时该告诉用户。显然,提醒是一个依赖于任务的弱实体。在cuckoo的代码中,任务是Task类的实例对象,有一个名为remind的成员变量存储着提醒。

但这样的结构不方便存储在MySQL中。遵守关系型数据库设计的第一范式,任务和提醒分别被存储在t_taskt_remind表中,二者经过t_task.remind_id联系起来。

固然,也能够在一开始就用MongoDB来存储这些数据(甚至能够用对象数据库?不过我没玩过)。尤为是cuckoo只是一个小玩意儿,MySQL和MongoDB都足以胜任。但做为一名有理想的程序员,在作设计的时候,不该该让低层细节过度干预高层策略。(在《Clean Architecture》中,越是接近I/O的越是low-level,反之则是high-level。)

面向业务逻辑,而非存储结构

业务逻辑和规则才是一个服务的核心,应该把更多精力花在实现业务逻辑的数据结构和算法上。

以网上商城中常见的优惠券功能为例。优惠券服务所管理的优惠券每每有着各类效果、条件,以及限制。为了保持灵活性,优惠券类(下称Coupon)的实例对象中会有三种接口类型的成员变量:

  1. Effect类型的变量effect,负责实现优惠效果的计算逻辑;
  2. Condition数组类型的变量conditions,负责实现使用条件的检查逻辑;
  3. Restriction数组类型的变量restrictions,负责实现使用限制的检查逻辑。

三个接口能够有各类各样的实现——定额减免、折扣减免、某年月日前可用、不可用于电子产品,等等。如此,优惠券功能具有了极大的灵活性,业务能够为所欲为,产品能够随心所欲,老板数钱数到手软,公司业绩蒸蒸日上。

那么如何存储EffectConditionRestrictionCoupon类的实例对象呢?没有惟一的选择,既能够存储在MySQL中,也能够存储在MongoDB中,或者别的什么数据库中。无论这些数据最终如何持久化,都不会影响做为高层策略的优惠券业务逻辑。反过来,若是在代码中处理的不是类、接口,以及实例对象,而是直接从数据库中取出来的、贫血模型的行(或文档),处理起来就不是很优雅了——能够预见 ,代码中会充斥着许多的if-else判断逻辑。

数据库只是帮忙从磁盘中读取数据的软件,它的schema不该该直接成为应用的数据模型。

Interface Segregation Principle

不该该在HTTP接口的响应中直接暴露数据库的schema。

不说别的,光是数据库schema与接口规格所使用的命名规则就足以形成差别了。也许在MySQL中用snake case命名一列,却又在HTTP响应的JSON对象中用camel case命名字段。

此外,除非这些接口仅仅实现增删查改、没有任何的业务逻辑或规则,不然一个服务更应当提供与业务需求刚好契合的接口。仍然以上文的优惠券服务为例,尽管内部可能EffectConditionRestrictionCoupon等诸多概念,但煮不在意用户不在意,他们只想看到用人话说出来的优惠券效果以及使用规则——用户甚至不关心条件和限制有何不一样。

若是优惠券服务直接将数据库中的行(或文档)序列化成JSON返回给调用者,会致使封装的泄露。每个查询优惠券的调用方,都必须了解优惠券的内部表示形式,必须知道效果由effect描述、用券后的订单金额是多少、conditions中有关于过时与否的信息,等等。每增长一个优惠券服务的使用者,就相应地增长一套描述这些内容的代码。甚至当优惠券服务自身重构的时候,也许牵连到众多的调用方。

若是直接将存储结构暴露给调用者的话,又何须再作一个Web服务呢。

切勿矫枉过正

的确存在这样的例子,数据库schema、数据模型,以及HTTP响应结构三者相同。这是由于比起维护数据库schema与数据模型的转换规则,以及DTO与数据模型的转换规则而言,在领域代码中直接使用数据库schema来表达数据模型的成本更低一点。尽管数据库schema不是Web服务的一切,但不少时候能够因地制宜地妥协一下。

阅读原文

相关文章
相关标签/搜索