跳至内容
3.1 从业务语言提取数据库事实

3.1 从业务语言提取数据库事实

建模会议最容易从“订单表有哪些字段”开始,最后得到一个容纳所有词汇的大表,却没人能说明哪种状态算错。本节反过来:先把业务陈述改写成可判断真假的事实,再找出必须永久成立的不变量。

3.1.1 实体、事件、状态与业务不变量

四类词回答不同问题:

概念问题pg36_shop 例子常见误区
实体哪个事物拥有持续身份?customer、product、sales order看到名词就机械建表
事件在什么时刻发生了什么?order placed、payment attempted只保留当前状态,失去历史事实
状态某实体当前处于什么条件?order is placed/paid/cancelled把一个 text 字段当成完整状态机
不变量哪些命题在每次提交后都必须为真?order 指向已有 customer只写“通常”“应该”,没有失败语义

表与业务概念不是一一映射。一个实体可能需要多个关系保存当前事实和历史;多个小值对象也可能嵌入同一关系。判断依据是身份、生命周期、基数、更新原子性和查询责任,而不是面向对象类图。

把叙事改写成事实句

“Alice 买了咖啡和杯子并支付成功”至少包含:

  1. customer CUST-ALICE 在下单时存在;
  2. order ORD-20260729-0001 属于该 customer;
  3. order 接受了两个不同 line;
  4. 每个 line 记录数量与购买时单价;
  5. line 引用的 product 在接受订单时存在;
  6. payment provider 接受了一个金额为 167.80 的尝试;
  7. provider reference 在该 provider 范围内唯一;
  8. captured payment 总额与订单接受金额相符;
  9. “paid” 状态只有在满足支付规则后才能出现。

前七项可由本章五个关系直接表达;第八、九项是跨表和状态转换规则,v0 会故意暴露它们尚未关闭。

不变量要写出四个维度

不要只写“订单号唯一”,而要写:

规则:order_no 在 pg36_shop 订单域内唯一。
时点:每条 INSERT/UPDATE 语句结束时成立。
权威:PostgreSQL unique constraint。
违反:整条语句失败;调用方收到 unique_violation。

再看“订单至少有一行”:

规则:进入 accepted/paid 等已接受状态的订单至少有一个 line。
时点:状态转换事务提交时成立。
权威:订单命令边界;实现方式待 ch04/ch13 决定。
违反:状态转换失败,订单不得部分提交。

这条规则不能要求每次刚插入 order 行后就成立,否则同一事务尚未来得及插入第一个 line。时点是模型的一部分。

事件和状态不要互相伪装

payment 在 v0 中代表一次有身份的支付尝试,包含 provider reference、request fingerprint、状态和发生时间。它不是完整的支付事件流;若未来要回答每次授权、捕获、撤销和退款的顺序,需要追加事件关系或对账记录,而不是在一行上反复覆盖后声称历史仍然存在。

同样,order_status = 'paid' 只是一个断言。只有定义允许值、转换、终态、并发规则和金额条件后,它才成为可依赖状态机。ch04 负责值域表达,ch10 处理并发转换,ch13 讨论数据库逻辑边界。

3.1.2 命令模型、查询模型与数据所有权

命令模型回答“怎样接受一个合法变化”,查询模型回答“消费者怎样读取所需形状”。它们可以共享一套规范事实,但不必共享一张宽表。

规范事实与读取形状

v0 的命令侧关系是:

  • customer:当前客户档案;
  • product:当前商品目录;
  • sales_order:订单头、客户引用、幂等输入和下单快照;
  • sales_order_item:组成关系与购买时商品快照;
  • payment:支付尝试与外部标识。

查询侧提供 shop_api.order_summary

SELECT
    order_id,
    order_no,
    order_status,
    item_count,
    item_subtotal,
    captured_amount
FROM shop_api.order_summary
ORDER BY order_id;

item_count 与金额汇总按需计算,不在订单头重复保存。现在两行三项数据,普通 view 足够;未来是否物化、缓存或拆到读取服务,要由查询量、延迟和新鲜度目标证明。

这不是要求每个系统都采用 CQRS。核心原则更朴素:写模型首先维护事实与不变量,读接口可以投影、连接和聚合;不要为了一个列表页面,让五处写入共同维护一张含所有派生字段的表。

数据所有权不是数据库 owner

“谁拥有数据”至少有三层含义:

层次pg36_shop 例子责任
业务权威订单域拥有已接受订单事实决定语义、修改接口与生命周期
PostgreSQL 对象 ownerpg36_owner执行 DDL、授权、迁移
运行角色pg36_apppg36_ro按最小权限执行命令或读取

三者不能混为“这个服务有数据库密码,所以它拥有一切”。

业务事实清单把本章范围写成:

事实本章权威外部依赖
当前客户档案pg36_shop身份系统可能提供外部标识
当前商品目录pg36_shop 教学范围真实组织可能有独立商品服务
已接受订单与购买快照pg36_shop下单客户端只提交命令
本地支付尝试记录支付边界provider 才是资金处理外部权威

数据库能保证 provider reference 在本地不重复,却不能证明第三方真的扣款。外部响应必须经过认证、重试、对账与补偿;跨系统一致性不能伪装成一个本地外键。

一个事实只应有一个写入责任

多个服务可以消费订单摘要,但不应绕过订单命令随意更新 order_status。否则每个写者都带着不同规则,数据库最后只能保存“谁最后提交”的结果。

若组织确实需要多写者,必须共享同一数据库不变量、并发协议与发布契约。更常见的选择是一个权威写入口,其他服务通过 API、消息或受控数据库接口提出命令。

所有权还包括删除与保留。客户档案删除不意味着历史订单必须消失;商品下架不意味着订单快照应级联删除。关系动作要服从业务生命周期,而不是代码生成器默认值。

3.1.3 哪些规则必须由数据库兜底

“都放应用”与“都放数据库”同样偷懒。选择执行层时逐条问:

  1. 违反后是否会形成永久无效数据?
  2. 是否可能由并发写者同时触发?
  3. 是否存在多个写入入口、批处理或人工 SQL?
  4. PostgreSQL 能否在正确时点原子判断?
  5. 规则是否依赖外部系统或人工裁决?
  6. 错误需要怎样映射给调用方?

数据库应兜底的最小集合

规则v0 PostgreSQL 表达原因
每行有稳定身份PRIMARY KEY所有写入口共享
SKU/order_no 等业务键不重复UNIQUE并发下应用“先查再插”会竞态
order 必须有 customerFOREIGN KEY防止孤儿引用
line 必须有 order 与 product两条 FK关系事实必须真实
line_no、quantity 为正CHECK单行、确定、无外部依赖
关键列必须存在NOT NULL让“未知”成为显式建模决定

应用仍应提前验证并返回友好错误,但数据库约束是最后防线。它覆盖后台任务、迁移脚本、并发请求和未来尚未出现的写者。

普通约束不适合什么

PostgreSQL 不支持让 CHECK 引用该行之外的表数据并承诺持续一致。下面的想法是错误方向:

-- 不要这样设计跨表 CHECK
CHECK (
  amount <= (
    SELECT sum(unit_price * quantity)
    FROM shop.sales_order_item
    WHERE order_id = payment.order_id
  )
)

CHECK 按新行或更新行验证,并假设表达式对同一行输入保持不变。其他表以后变化时,它不会自动重检;dump/restore 顺序也可能让这种伪约束失败。

跨表规则的候选实现包括:

  • 同一事务中的原子命令与显式锁;
  • 唯一、外键、排他约束等真正受支持的关系约束;
  • 受严格设计的触发器或延迟约束触发器;
  • 由状态转换把“草稿不完整”和“已接受必须完整”分开;
  • 外部工作流的对账、补偿和人工裁决。

选择触发器不自动让规则正确;并发、递归、批量导入、错误语义和恢复都要验证。ch10 与 ch13 会继续。

v0 的刻意空缺

本章负向实验会证明以下状态仍能提交到事务内:

  • numeric 接受 9 位小数;
  • order_status 接受 teleported
  • 一个 status 为 paid 的 order 可以没有 line 和 payment。

实验随后 ROLLBACK,不会污染基线。暴露空缺比用 prose 声称“以后应用会注意”更诚实;它们进入四项未决登记并在 ch04 验收。

本节产物

为每条业务规则建立最小登记:

rule_id
fact / invariant
scope
validation_time
authoritative_owner
enforcement_layer
error_semantics
evidence_query
open_questions

如果一条规则没有权威 owner 或验证时点,先不要写 DDL。技术不能替组织替你决定事实。

本节验收

  • 能把一个业务故事拆成实体、事件、状态和至少五条事实;
  • 每条不变量都写出范围、时点、权威与违反结果;
  • 命令模型与查询投影职责分开;
  • 业务 owner、对象 owner 与 runtime role 不再混用;
  • 能解释哪些规则适合约束,哪些需要事务或外部协调;
  • 所有未闭合规则进入登记,而不是藏在代码注释。

参考资料


返回本章目录 · 下一节:标识、主键与业务键 · 查看全书目录 · 查看索引中心

最后更新于