3.1 从业务语言提取数据库事实
建模会议最容易从“订单表有哪些字段”开始,最后得到一个容纳所有词汇的大表,却没人能说明哪种状态算错。本节反过来:先把业务陈述改写成可判断真假的事实,再找出必须永久成立的不变量。
3.1.1 实体、事件、状态与业务不变量
四类词回答不同问题:
| 概念 | 问题 | pg36_shop 例子 | 常见误区 |
|---|---|---|---|
| 实体 | 哪个事物拥有持续身份? | customer、product、sales order | 看到名词就机械建表 |
| 事件 | 在什么时刻发生了什么? | order placed、payment attempted | 只保留当前状态,失去历史事实 |
| 状态 | 某实体当前处于什么条件? | order is placed/paid/cancelled | 把一个 text 字段当成完整状态机 |
| 不变量 | 哪些命题在每次提交后都必须为真? | order 指向已有 customer | 只写“通常”“应该”,没有失败语义 |
表与业务概念不是一一映射。一个实体可能需要多个关系保存当前事实和历史;多个小值对象也可能嵌入同一关系。判断依据是身份、生命周期、基数、更新原子性和查询责任,而不是面向对象类图。
把叙事改写成事实句
“Alice 买了咖啡和杯子并支付成功”至少包含:
- customer
CUST-ALICE在下单时存在; - order
ORD-20260729-0001属于该 customer; - order 接受了两个不同 line;
- 每个 line 记录数量与购买时单价;
- line 引用的 product 在接受订单时存在;
- payment provider 接受了一个金额为 167.80 的尝试;
- provider reference 在该 provider 范围内唯一;
- captured payment 总额与订单接受金额相符;
- “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 对象 owner | pg36_owner | 执行 DDL、授权、迁移 |
| 运行角色 | pg36_app、pg36_ro | 按最小权限执行命令或读取 |
三者不能混为“这个服务有数据库密码,所以它拥有一切”。
业务事实清单把本章范围写成:
| 事实 | 本章权威 | 外部依赖 |
|---|---|---|
| 当前客户档案 | pg36_shop | 身份系统可能提供外部标识 |
| 当前商品目录 | pg36_shop 教学范围 | 真实组织可能有独立商品服务 |
| 已接受订单与购买快照 | pg36_shop | 下单客户端只提交命令 |
| 本地支付尝试记录 | 支付边界 | provider 才是资金处理外部权威 |
数据库能保证 provider reference 在本地不重复,却不能证明第三方真的扣款。外部响应必须经过认证、重试、对账与补偿;跨系统一致性不能伪装成一个本地外键。
一个事实只应有一个写入责任
多个服务可以消费订单摘要,但不应绕过订单命令随意更新 order_status。否则每个写者都带着不同规则,数据库最后只能保存“谁最后提交”的结果。
若组织确实需要多写者,必须共享同一数据库不变量、并发协议与发布契约。更常见的选择是一个权威写入口,其他服务通过 API、消息或受控数据库接口提出命令。
所有权还包括删除与保留。客户档案删除不意味着历史订单必须消失;商品下架不意味着订单快照应级联删除。关系动作要服从业务生命周期,而不是代码生成器默认值。
3.1.3 哪些规则必须由数据库兜底
“都放应用”与“都放数据库”同样偷懒。选择执行层时逐条问:
- 违反后是否会形成永久无效数据?
- 是否可能由并发写者同时触发?
- 是否存在多个写入入口、批处理或人工 SQL?
- PostgreSQL 能否在正确时点原子判断?
- 规则是否依赖外部系统或人工裁决?
- 错误需要怎样映射给调用方?
数据库应兜底的最小集合
| 规则 | v0 PostgreSQL 表达 | 原因 |
|---|---|---|
| 每行有稳定身份 | PRIMARY KEY | 所有写入口共享 |
| SKU/order_no 等业务键不重复 | UNIQUE | 并发下应用“先查再插”会竞态 |
| order 必须有 customer | FOREIGN 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 不再混用;
- 能解释哪些规则适合约束,哪些需要事务或外部协调;
- 所有未闭合规则进入登记,而不是藏在代码注释。
参考资料
返回本章目录 · 下一节:标识、主键与业务键 · 查看全书目录 · 查看索引中心