BodySense 数据库第 1 课:Database Ownership、关系模型与 Durable Truth
从 BodySense 当前 PostgreSQL 18 schema 出发,建立数据库作为 durable business truth 的心智模型,并区分表、关系、投影、缓存与事件日志的 ownership。
[!info] related notes
- 课程 MOC:BodySense 数据库工程深化课程 MOC
- 知识地图:数据库 MOC
- 核心概念:关系型数据库、Schema
- 项目:BodySense 数据库设计
BodySense 数据库第 1 课:Database Ownership、关系模型与 Durable Truth
[!abstract] 本课目标 先不学索引和事务细节,先回答数据库工程最根本的问题:为什么这一份数据应该由 PostgreSQL 持有,它究竟承诺了什么?
从零建立概念:Database、DBMS、Relational Database 到底分别是什么
Database:被系统长期管理的一组数据
最宽泛地说,Database 是一组按照某种模型组织、需要被长期读取和修改的数据。
但工程里我们通常不会让业务进程自己:
open file
seek bytes
自己处理并发写
自己处理 crash recovery
自己维护索引
而是交给一个专门的软件。
DBMS:真正替应用管理数据的软件
DBMS = Database Management System。
PostgreSQL 才是 DBMS。
BodySense Go API 并不是直接“打开一个数据库文件”,而是:
Go API
↓ SQL / PostgreSQL wire protocol
PostgreSQL Server
↓
tables / indexes / WAL / files
DBMS 负责的事情包括:
解析 SQL
管理连接
执行查询
管理 transaction
控制并发
维护 constraint
管理 page / buffer
维护 index
记录 WAL
crash recovery
所以“会用数据库”不是只会 SELECT,而是逐渐理解这些能力怎样共同保证业务事实。
Relational Database:关系模型不是“表格长得像 Excel”
Relational 来自 Relation。
初学时可以把 Relation 暂时近似成 Table,但它背后的重要思想是:
一类事实
→ 由固定属性描述
→ 每一行是一条事实
→ 行之间通过 Key 建立关系
→ 查询通过关系运算组合事实
例如:
users
→ 用户事实
conversations
→ 会话事实
→ user_id 引用 users
messages
→ 消息事实
→ conversation_id 引用 conversations
这里不是“一个用户对象里面嵌一个 messages 数组”这么简单,而是把不同生命周期、不同基数的事实拆成独立 Relation。
Schema 与 Data:规则和当前内容是两层东西
Schema
Schema 描述数据库允许什么形状的数据存在:
有哪些表
有哪些列
列是什么类型
谁是主键
谁引用谁
哪些字段必填
哪些组合唯一
哪些值必须满足 CHECK
有哪些 index
Data / Instance
Data 是此时此刻真正存在的行。
例如:
Schema:
users(id uuid primary key, email text unique ...)
Data:
(u1, arlon@example.com)
(u2, ...)
这一差异后面会直接连接 Migration:
Model / Schema definition
≠
已经存在的生产数据状态
为什么业务服务需要 Durable Truth
Durable 这里先理解成:
一旦业务已经确认这个事实成立,它不能因为 API 进程重启就消失。
例如:
用户完成 Assessment
Diagnosis 被正式接受
Treatment revision 被保存
Job 已被创建
RuntimeEvent 已经告诉前端
这些都不能只放 Go memory。
如果 API 重启后全部消失:
HTTP 200 success
↓
process restart
↓
事实不存在
整个业务 contract 就不成立。
因此 PostgreSQL 在 BodySense 中最核心的职责不是“比文件方便”,而是成为 durable business truth 的主要拥有者。
Source of Truth / Authority / Projection 三个词
Source of Truth
对某一事实来说,最终应该相信哪一份。
例如:
BodyState Fact
→ PostgreSQL row = truth
Authority
更强调“谁拥有最终裁决权”。
例如:
数据库 UNIQUE constraint
vs
Go 代码里先 SELECT 看有没有重复
真正并发最终裁决 uniqueness 的 authority 是数据库 constraint。
Projection
Projection 是为了某个读取场景重新组织出来的数据形状。
normalized tables
→ JOIN / aggregate / select latest
→ HealthWorkspace response
Projection 可以:
- 临时计算;
- 缓存;
- 持久化成 read model;
但要明确:
Projection 是由哪些 truth 推出来的?失效后能不能重建?
为什么“高频读取”不能决定 Ownership
假设用户姓名每秒读一百万次。
这只能说明:
可能需要 Cache / Replica / Projection
不能说明:
姓名应该从 PostgreSQL 搬成 Redis-only
Ownership 要看:
lifetime
durability
constraints
relations
audit
recovery
concurrency semantics
Performance 是设计维度之一,不是唯一维度。
BodySense 的最小数据分层
可以先用这张图:
Browser local/UI state
→ 临时交互
Redis
→ cache / fast runtime state / coordination
PostgreSQL
→ durable domain truth / audit / replay / job ledger
Object Storage
→ 大型 binary bytes
后续课程会不断用这张 ownership 图判断“某个技术能不能替代另一个技术”。
1. Database 不是“存 JSON 的地方”
关系数据库同时提供四类能力:
Durable Facts → 数据跨进程/重启长期存在
Constraints → 非法状态直接被数据库拒绝
Relations → PK/FK/Cardinality 表达实体关系
Transactions → 多个读写形成一致提交边界
BodySense 的 BodyState、Diagnosis、Treatment、Runtime Event、Job 等之所以进入 PostgreSQL,不只是因为“需要保存”,而是因为它们需要长期事实、关系约束、审计、事务或恢复能力。
2. Relation / Row / Column / Key
把表理解成某类事实集合,而不是 Go struct 的磁盘镜像:
Table = 一类事实
Row = 一个事实实例
Column = 该事实的属性
Primary Key = 行身份
Foreign Key = 与另一类事实的引用关系
Constraint = 哪些数据库状态根本不允许存在
ORM model 只是应用层表示;真正的持久化 contract 在数据库 schema。
3. Source of Truth 与 Projection
BodySense 中应明确:
PostgreSQL durable table
= authoritative fact
Redis cache / TanStack Query
= derived copy
HealthWorkspace / ThreadProjection
= read projection
Projection 可以被重建,durable truth 不能因为 projection 更方便就倒置 ownership。
4. BodySense 真实锚点
从以下文件阅读:
apps/api/migrations/000001_vnext_baseline.up.sql
apps/api/internal/model/body_state.go
apps/api/internal/model/runtime_event.go
apps/api/internal/model/job.go
观察哪些字段是:身份、状态、时间、关系、revision、JSONB,以及哪些不变量已经下沉到数据库。
Prediction
[!question] 先回答
BodyState.current_revision和前端当前选中的 tab,哪一个更适合 PostgreSQL?为什么?- 如果
HealthWorkspace可以完全由多个 durable 表重新拼出来,它应该是 authority 还是 projection?- 一个字段每秒读取一万次,是否足以说明 Redis 应成为它的 source of truth?
- Go struct 上写了
Status string,是否等于数据库已经限制 status 只能出现合法值?- Runtime Event 需要 reconnect 后从
after_seq恢复,这个要求对存储 ownership 有什么暗示?
[!success]- Answer / 解析 1.
BodyState.current_revision。 它参与持久化并发控制与历史版本语义;tab 是 UI 本地状态。是否持久化取决于业务 ownership/lifetime,而不是值看起来是否简单。2. Projection。 它是读优化/界面形状,可以重建;真正事实仍在其来源表。
3. 不能。 高频只说明可能值得缓存;source of truth 取决于 durability、约束、恢复和业务语义。
4. 不等于。 Go 类型约束只影响应用路径;绕过应用、并发写或未来代码仍可能写入任意字符串。需要 CHECK/ENUM/外键等数据库约束才能成为持久化 invariant。
5. 暗示事件必须有 durable、可排序、可按 cursor 查询的事实源。 瞬时 Pub/Sub 可以做 wakeup,但不能替代 durable event ledger。
Lab
从 baseline migration 任取三张表,写一张 ownership 表:
| 数据 | Authority | 可重建? | 需要约束? | 需要历史/审计? |
|---|---|---|---|---|
| 示例 | PostgreSQL | 否 | 是 | 是 |
L4 Gate
不看笔记,解释一个 BodySense 页面上看到的数据,沿 UI → API → Repository → PostgreSQL 指出哪一步只是 projection,哪一步才是 durable truth。