领域模型优先于数据库表


由 Mark Seemann 发布:在讨论数据库,特别是 ORM 时,有些人会不言而喻地假设关系数据库是存储数据的唯一选择。

许多程序员在关系数据设计方面非常熟练,他们在思考新问题时自然会使用这些技能。

但是请尝试在不考虑存储的情况下对业务问题进行建模,看看结果会如何。测试驱动开发对于此类任务来说通常是一种很好的技术。然后,一旦你有了一个好的 API,就考虑如何存储数据。您以这种方式开发的领域模型可能自然会建议一种存储和检索数据的好方法。

订单案例
我的编程生涯的前四年都花在开发网上商店上。订单是这项工作的一个组成部分。

订单是一份文件(类似法律合同):您不希望客户的地址在事后可更新。

使用规范化的(数据库表)关系模型:
在有订单、订单行、人员、地址和城市的情况下,需要提前加载所有行,将其映射到对象并设置引用以创建对象图,这样才能根据人员的地址显示运费。

(上述图是一个订单的数据库表ER关系模型图)
这里客户的地址是直接指向了其客户关系数据库中的地址表,如果客户在下单后,更改了地址行,订单就发往了新地址,但是订单是一份文件,下单后其中信息也是应该不可变的。

同样,订单行中产品也不应该直接指向产品目录数据表中的实际产品条目。

您至少应该对数据库模型进行非规范化:隐含的订单具有订单行,这些订单行是相关产品数据的复制副本,而不是直接链接到产品目录。

var order = new Order(
    new Person("Olive", "Hoyle",
        new Address(
"Green Street 15", new City("Oakville"), "90125")),
        new OrderLine(123, 1),
        new OrderLine(456, 3),
        new OrderLine(789, 2));

有了这样的代码,许多DDD爱好者就会开始谈论聚合根,但坦率地说,这个概念对我来说从来没有多大意义。

相反,上面order是由不可变数据结构组成的树。它简单地序列化为例如 JSON:

{
  "customer": {
   
"firstName": "Olive",
   
"lastName": "Hoyle",
   
"address": {
     
"street": "Green Street 15",
     
"city": { "name": "Oakville" },
     
"zipCode": "90125"
    }
  },
 
"orderLines": [
    {
     
"sku": 123,
     
"quantity": 1
    },
    {
     
"sku": 456,
     
"quantity": 3
    },
    {
     
"sku": 789,
     
"quantity": 2
    }
  ]
}

所有这些都强烈表明,使用文档数据库而不是关系数据库来存储和检索这类数据要容易得多。

对于大多数在线事务处理系统来说,关系数据库并不一定是最佳选择。

首先 开发领域模型的全部目的是找到一种以鼓励正确性和易用性的方式表示业务问题的好方法。

如果您向我提供一个关系模型,但没有描述您想要实现的业务目标,那么我就没什么可做的了。

在许多情况下,在我看来,程序员似乎从关系模型开始,只是继续抱怨它很难在面向对象(或函数)代码中使用。

如果您从业务问题开始并弄清楚如何在代码中对其进行建模,那么存储数据的最佳方法可能会不言而喻。文档数据库通常很适合,事件存储也是如此。

关系数据库适合数据输出报告
虽然我不再认为关系数据库特别适合在线事务处理,但它们确实擅长一件事:即席查询。因为它是一种丰富且成熟的技术,并且SQL是一种功能强大的语言,所以您可以通过多种方式对数据进行切片和切块。

这使得关系数据库对于报告和其他类型的数据提取任务非常有用。

如果拥有关系数据库的唯一目的是支持报告,您可以考虑将其设置为辅助系统。将您的在线交易数据保存在另一个系统中,但定期将其同步到关系数据库。如果关系数据库的唯一目的是支持报告,则可以将其视为只读系统。这使得同步变得易于管理。一般来说,如果可能的话,您应该避免双向同步,但单向同步通常不是一个问题。

封装优先
我使用的大多数系统都不是CRUD系统,而是正确性很重要的系统。举个例子,我的一个客户从事安全性较高的数字基础设施。在我职业生涯的早期,当这些系统还是新的时候,我帮助编写了网上商店。让我告诉您:系统所有者非常关心价格是否正确,以及订单的接受和处理没有错误。

在我的《适合你头脑的代码》一书中,我试图通过附带的示例代码来捕捉此类系统的本质,该代码假装是一个在线餐厅预订系统。虽然这听起来像是一个平常的 CRUD 系统,但业务逻辑并不完全简单。

当优先考虑封装时,您应该能够使用任何设计模式、运行时断言以及静态类型系统(如果您使用这种语言)来保护正确性。

您应该能够组合对象、定义值对象、包装单个值以避免原始痴迷、使构造函数私有、利用多态性并有效地使用您的语言、习惯用法和平台提供的任何技巧。

如果您想使用Church 编码Visitor 模式来表示 sum type,您应该能够做到。

在编写此类系统时,我从域模型开始,没有考虑如何保存或检索数据。

根据我的经验,一旦领域模型开始凝结,持久性问题往往会自行回答。通常有一两种明显的方法来存储和读取数据,这时,通常的关系数据库不是最明显的选择。

编写最好的 API 来解决问题,然后弄清楚如何存储数据:这就是持久性无知(persistence ignorance)。
只要你撒下比关系数据库更广的网,它就会变得比传闻中更容易。