从内存数组到 PostgreSQL + Prisma — 持久化、数据建模与迁移
从内存数组到 PostgreSQL + Prisma — 持久化、数据建模与迁移
这一章讲什么: 前面 API 的用户和帖子都躺在内存数组里,服务器一重启就蒸发。 这一章给应用装上真正的记忆:PostgreSQL 存数据,Prisma 当翻译和管家。 读完你会看懂数据库设计的通用词汇——表、主键、外键、索引、约束、迁移—— 以及「改一次 schema,数据库怎么跟着演化」这套贯穿整个软件生命周期的流程。
1. 顶层全景:为什么需要数据库,链条上每一环管什么
持久化问题一句话:内存随进程一起死,数据库让数据活过重启、崩溃和部署1。 数据库有好几类:SQLite 这类文件库、Redis 这类键值库、MongoDB 这类文档库、 PostgreSQL/MySQL 这类关系库,还有图数据库、向量数据库(存「语义向量」、按相似度搜的库)、时序数据库等 专库专用的角色2。本书选关系型数据库,因为结构化业务数据它最常用, 且用约束和事务强制数据一致性——这正是多数业务应用要的东西2。
Express 路由 ──► Prisma Client(类型安全的查询方法)
│ 生成 SQL
▼
PostgreSQL(存数据、强制约束)
▲
Prisma Migrate(schema 演化的账本)
图说:schema.prisma 是唯一事实源——类型、表结构、查询 API 都从它生成。ORM(对象关系映射)坐在应用代码和数据库之间:你写
prisma.user.findUnique({ where: { id: 1 } }),它翻译成 SQL、发给数据库、
把结果映射回带类型的对象3。换来的是类型安全、自动补全、迁移管理、
以及内置防 SQL 注入3。ORM 之间有三条分类轴:模式
(Active Record 的 user.save() vs Data Mapper 的独立持久层)、
路线(code-first / schema-first / database-first)、抽象层级
(Knex 这类轻查询构造器 vs Prisma 这类全功能 ORM)——Prisma 属于
Data Mapper、schema-first、全功能3。
2. 关系型数据库五件套 + 两个关键取舍
- 表/行/列:数据按表组织;列定义形状(schema),行是具体记录。 schema 由数据库自己强制执行——不是应用代码求它遵守,而是它天生不让 你违反4。
- 主键(primary key):每行的唯一标识。外键(foreign key):
一表里引用另一表主键的列(
posts.authorId→users.id)。 数据库通过约束强制引用完整:往一个不存在的用户名下插文章,直接被拒; 删一个还有文章的用户,按配置选择拦下、级联删文章、或把外键置空4。 - 索引(index):像书后的索引——按值直接跳到目标行,不用整表扫。 代价是占磁盘、写略慢。主键和唯一约束自动有索引,外键列没有—— 常用它查询就得自己建4。
- 事务与 ACID:Atomicity(一组操作要么全成要么全败)、Consistency (每次只从一个合法状态到另一个合法状态)、Isolation(并发事务互不干扰)、 Durability(提交过的数据抗崩溃)5。
- SQL:关系库的通用语言。建表
CREATE TABLE、插INSERT、查SELECT … WHERE、改UPDATE、删DELETE、跨表JOIN—— Prisma 的每个方法都对应其中一种,看它生成的 SQL 是排障基本功6。
两个本章反复用的取舍:
自然键 vs 代理键。 主键不一定要自增整数,可以用 email 这种「自然值」—— 但一旦它变了(用户改邮箱、公司合并换编码),所有引用它的表、所有日志 都得跟着级联改;而且变长字符串做主键,索引更大、JOIN 更贵; 还有隐私问题:PII(个人可识别信息)散在几十张表和日志里,脱敏(抹掉敏感字段)变噩梦。 实务口径:主外键关系用代理键(自增整数或 UUID),自然键用唯一约束保住 业务规则——稳定领域(国家码、货币码)例外7。
float vs decimal。 0.1 + 0.2 === 0.30000000000000004,因为 float
用二进制存小数,0.1 没有精确的二进制表示(就像 1/3 没有精确的十进制)。
价格、余额、一切金融数字,永远用 decimal——它按十进制位存储8。
同理:自增主键用 PostgreSQL 的 SERIAL(1, 2, 3…);UUIDv7 是另一个现代
选项(内置时间戳(记录精确到秒的时间)、有序、多台机器不用协调生成),v7 需要 PostgreSQL 18+,
更早版本用 gen_random_uuid()(随机、失序)9。
3. PostgreSQL 与 Prisma 7:为什么是它们,新在哪
PostgreSQL 是 1986 年 UC Berkeley 研究项目起家的开源(源码公开、可自由使用和修改)关系库,Apple、 Instagram、Spotify、Twitch 都在用;选它不只因为流行——JSON/JSONB 列、 全文检索、数组列、多种索引类型(加速查询的数据结构)、可插扩展(PostGIS 地理、pgvector 向量), 加上 TypeScript 生态一等公民支持10。
Prisma 7(2025 年 11 月发布)的架构变化值得单独记,因为它解释了
后面所有配置为什么这么写:1–6 代的查询引擎是 Rust 写的独立二进制,
每次查询要跨进程边界序列化(转成字节再传)一次;7 代把 Rust 引擎换成跑在 Node 进程内的
TypeScript/WASM 查询编译器,生成 SQL 后直接经标准 pg 驱动发给
PostgreSQL——没有序列化边界,没有独立进程11。官方基准称比 6 代快最多
3 倍,社区独立测试没能一致复现这个数字;但部署包小得多(不用带 Rust
二进制)、双运行时序列化开销消失是确定的11。配套变化:
- 连接走 driver adapters(驱动适配器,把 Prisma 接到具体数据库驱动的转接头):
@prisma/adapter-pg包运行时传入, 连接串不再写进 schema 文件,改由prisma.config.ts在运行时提供12; - 生成的 Client 放进你的源码树(
src/generated/prisma),不再藏在node_modules,导入路径清晰、代码可见13; .env不再自动加载:prisma.config.ts里必须显式import "dotenv/config",漏了这一行连不上数据库,报错还不直指原因14;- schema 文件里再写
url = env("DATABASE_URL")会直接报 P1012 错13。
4. 主线例子:四张表、三种关系、一次迁移账本
数据模型从第 9 章的内存版升级成真库版。User 是我们的第一块:
model User {
id Int @id @default(autoincrement())
username String @db.VarChar(100)
email String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}逐属性读:@id 主键;@default(autoincrement()) 让 PostgreSQL 自动
分配递增整数;@db.VarChar(100) 是原生类型注解——把 Prisma 的
String 落成 VARCHAR(100)(注意:PostgreSQL 里 VARCHAR 和 TEXT
同存储,(100) 只是校验约束,不是性能优化);@unique 唯一约束;
@default(now()) 建行时间戳(精确到秒的创建时间);@updatedAt 由 Prisma 在应用层维护
——直接拿 SQL 改库,这个字段不会动15。
然后三种关系各建一个,这是本章的骨架:
| 关系 | 写法 | 数据库里的实际形态 |
|---|---|---|
| 一对多(User–Post) | User 侧 posts Post[](虚拟字段,不落列);Post 侧 author User @relation(fields:[authorId], references:[id], onDelete: Cascade) | 外键列 + 你自己声明的 @@index([authorId])——外键不会自动有索引,漏了它就全表扫16 |
| 一对一(User–Profile) | Profile 侧 userId Int @unique——@unique 就是一对一的秘密;User 侧 profile Profile? 可空 | 唯一约束保证一个用户最多一份档案17 |
| 多对多(Post–Category) | 两侧都写 Xxx[] 数组——Prisma 认出隐式多对多,自动建连接表 _CategoryToPost;代码里只用 connect/disconnect/connectOrCreate,不碰它18 | 自动连接表 |
再加一个 enum Role { USER ADMIN MODERATOR }——PostgreSQL 会建成
原生枚举类型,数据库层强制取值合法18。
迁移是这套流程的账本。 npx prisma migrate dev --name init 做两件事:
比对 schema 和数据库现状,生成一个带时间戳的 SQL 文件
(prisma/migrations/<时间戳>_init/migration.sql),然后把它应用到数据库19。
看一眼生成的 SQL 会发现全是熟面孔:SERIAL、PRIMARY KEY、
UNIQUE INDEX19。之后每改一次 schema,账本就多一页——
部署到新环境时,Prisma 按顺序补跑所有没跑过的迁移,空库直接被带到
当前状态20。四条常用命令的分工:
| 命令 | 干什么 | 什么时候用 |
|---|---|---|
prisma migrate dev | 生成迁移并应用 | 开发期每次改 schema;之后记得 prisma generate(7 代不再自动) |
prisma migrate deploy | 只应用不生成、不询问、不重新生成 client | 部署流水线 |
prisma migrate reset | 删库、从头重放全部迁移、重跑 seed | 开发库乱了想重来;生产禁用 |
prisma db push | 直接把 schema 推到库,不留迁移历史 | 原型期高频改 schema;稳定后切回 migrate dev |
已入账的迁移不许改——它是账本,不是草稿。要加 Prisma 生成不了的
SQL(建扩展、写函数、回填数据),用 --create-only 生成后不应用,
手动补 SQL,再跑一次 migrate dev 应用;要撤销已应用的变更,
写一个新迁移去对冲22。
5. 作者的判断与证据
- 「代理键为主、自然键作唯一约束」是明确的实务口径,理由(变更级联、 索引宽度、PII 扩散)逐条给出7。
- 「外键必须自己建索引」带性能机制说明(全表扫),不是风格偏好16。
- 「Prisma 7 官方 3 倍快未被社区一致复现,但架构方向明确进步」—— 对厂商基准保持了罕见的怀疑口径,值得记住11。
- 「float 不碰钱」是硬规则表述8。
6. 边界与局限
- SQL 只给了六味最小集;查询优化(执行计划、
EXPLAIN)整个话题缺席。 - 索引策略点到为止:五种 PostgreSQL 索引类型(B-tree/GIN/GiST/SP-GiST/ BRIN/Hash)列了名字,何时选哪种没说透9。
- Prisma 7 的时点性最强:配置写法(config.ts、驱动适配器、生成目录) 全是 7 代特有;旧教程的写法在 7 下会直接报错,迁移时务必对照官方 升级指南(本节为 2026 年中时点)。
判断(我们的,不是书里的): 本章最值得带走的心智是「schema 是唯一 事实源,迁移是它的账本」——这一句话同时解释了类型从哪来、表怎么建、 团队怎么协作、生产怎么部署。把「改字段 = 改文件 + migrate + generate 三连」变成肌肉记忆,比记住任何单个命令都值钱。 如果错,会错在: schema-first 的顺序对「从旧系统迁移数据」或 「数据库已存在且不归你管」的场景是别扭的——那两种场景 database-first 才是正解;本书默认绿地项目,这个前提没被明说。
7. 可带走的
- 内存数据随进程死;数据库的活是让数据活过重启、崩溃、部署。
- 表/列/行、主键/外键/索引/约束是关系库五件套;约束由数据库强制执行。
- 外键列不自动有索引,常用就要
@@index。 - ACID:原子、一致、隔离、持久——事务的四个性质。
- 主键用代理键;自然键用唯一约束;PII 别当外键撒得到处都是。
- 钱用 decimal,别用 float(0.1+0.2 的坑)。
- ORM 三条分类轴:模式(AR vs DM)、路线(code/schema/database-first)、 层级(查询构造器 vs 全功能);Prisma = DM + schema-first + 全功能。
- Prisma 7:WASM 查询编译器进程内跑,驱动适配器连库,config.ts 供连接串,
.env要手动 dotenv,generate 不再自动。 - 三种关系:一对多(虚拟字段 + 外键)、一对一(外键加 @unique)、 多对多(双侧数组,自动连接表)。
- 迁移是账本:dev 生成并应用、deploy 只应用、reset 重来(禁生产)、 db push 不留历史;已应用的迁移只新增、不回改。
8. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 持久化问题与数据库分类 | Understanding the persistence problem | text/46-fm-understanding-the-persistence-problem.txt:5(搜「symptom」) |
| 表/外键/索引 | What is a relational database? | text/47-fm-what-is-a-relational-database.txt:7(搜「tables」) · text/47-fm-what-is-a-relational-database.txt:41(搜「foreign keys」) · text/47-fm-what-is-a-relational-database.txt:81(搜「index at the back of a book」) |
| 自然键 vs 代理键 | What is a relational database? | text/47-fm-what-is-a-relational-database.txt:93(搜「Natural keys vs surrogate keys」) |
| ACID | What is a relational database? | text/47-fm-what-is-a-relational-database.txt:137(搜「ACID」) |
| SQL 六味 | A quick taste of SQL | text/51-fm-a-quick-taste-of-sql.txt:9(搜「fundamental SQL statements」) |
| ORM 分类 | What is an ORM and why use one? | text/48-fm-what-is-an-orm-and-why-use-one.txt:7(搜「Object-Relational Mapper」) · text/48-fm-what-is-an-orm-and-why-use-one.txt:27(搜「Active Record」) |
| PostgreSQL 选型理由 | What is an ORM and why use one? | text/48-fm-what-is-an-orm-and-why-use-one.txt:53(搜「UC Berkeley」) |
| float vs decimal | PostgreSQL data types and features | text/49-fm-postgresql-data-types-and-features.txt:79(搜「0.1 + 0.2」) |
| Prisma 三支柱 | What is Prisma? | text/52-fm-what-is-prisma.txt:11(搜「three core pillars」) |
| Prisma 7 WASM 架构 | Prisma 7 architecture | text/53-fm-prisma-7-architecture.txt:17(搜「TypeScript/WASM Query Compiler」) · text/53-fm-prisma-7-architecture.txt:29(搜「not consistently reproduced」) |
| 驱动适配器与连接串 | Prisma 7 architecture | text/53-fm-prisma-7-architecture.txt:37(搜「driver adapters」) |
| dotenv 必须显式导入 | Creating prisma.config.ts (new in Prisma 7) | text/57-fm-creating-prisma-config-ts-new-in-prisma-7.txt:50(搜「automatically loaded in Prisma 7.」) |
| schema 不再写 url | Anatomy of a Prisma Schema file | text/58-fm-anatomy-of-a-prisma-schema-file.txt:108(搜「P1012」) |
| User 模型各属性 | Building the user model | text/59-fm-building-the-user-model.txt:25(搜「id Int @id @default(autoincrement())」) · text/59-fm-building-the-user-model.txt:29(搜「updatedAt DateTime @updatedAt」) |
| 首次迁移与生成 SQL | Running our first migration | text/60-fm-running-our-first-migration.txt:13(搜「two things」) |
| 一对多与外键索引 | Adding the Post model (one-to-many) | text/61-fm-adding-the-post-model-one-to-many.txt:60(搜「virtual relation field」) · text/61-fm-adding-the-post-model-one-to-many.txt:112(搜「automatically create indexes on foreign 」) |
| 一对一与多对多 | Adding Profile and Category models | text/62-fm-adding-profile-and-category-models.txt:129(搜「constraint is what makes this a one-to-o」) · text/62-fm-adding-profile-and-category-models.txt:177(搜「implicit many-to-many」) |
| 枚举由数据库强制 | Adding Profile and Category models | text/62-fm-adding-profile-and-category-models.txt:213(搜「native enum」) |
| UUIDv7 取舍 | Alternative: UUIDv7 as a primary key | text/63-fm-alternative-uuidv7-as-a-primary-key.txt:11(搜「uuidv7()」) |
| 迁移即账本 | What are migrations? | text/64-fm-what-are-migrations.txt:3(搜「versioned SQL file」) |
| 四条迁移命令 | Key migration commands | text/66-fm-key-migration-commands.txt:9(搜「workhorse」) · text/66-fm-key-migration-commands.txt:35(搜「Never use this in production」) |
| 不回改迁移 | Customizing migrations | text/67-fm-customizing-migrations.txt:23(搜「Never edit」) |
Footnotes
-
出处:「Chapter 10: Database and Persistence with PostgreSQL + Prisma」第 7–25 段(
text/45-ch10-chapter-10-database-and-persistence-with-postgre.txt:23,搜「Where did all your data go」)。 ↩ -
出处:「Understanding the persistence problem」第 5–15 段(
text/46-fm-understanding-the-persistence-problem.txt:13,搜「categories」;:15,搜「enforce data consistency」)。 ↩ ↩2 -
出处:「What is an ORM and why use one?」第 3–15 段(
text/48-fm-what-is-an-orm-and-why-use-one.txt:7,搜「(Object-Relational Mapper) sits」;:13,搜「Active Record」)。 ↩ ↩2 ↩3 -
出处:「What is a relational database?」第 3–29 段(
text/47-fm-what-is-a-relational-database.txt:29,搜「enforced by the database itself」;:21,搜「foreign keys」;:29,搜「rejects the operation」)。 ↩ ↩2 ↩3 -
出处:「What is a relational database?」第 137 段(
text/47-fm-what-is-a-relational-database.txt:137,搜「ACID」)。 ↩ -
出处:「A quick taste of SQL」第 3–33 段(
text/51-fm-a-quick-taste-of-sql.txt:37,搜「maps directly」)。 ↩ -
出处:「What is a relational database?」第 41–61 段(
text/47-fm-what-is-a-relational-database.txt:107,搜「cascade that change」;:53,搜「PII」;:61,搜「surrogate keys」)。 ↩ ↩2 -
出处:「PostgreSQL data types and features」第 21–27 段(
text/49-fm-postgresql-data-types-and-features.txt:83,搜「0.30000000000000004」;:27,搜「always use decimal」)。 ↩ ↩2 -
出处:「PostgreSQL data types and features」第 31–39 段与「Alternative: UUIDv7 as a primary key」第 1–17 段(
text/49-fm-postgresql-data-types-and-features.txt:123,搜「uuidv7()」;text/63-fm-alternative-uuidv7-as-a-primary-key.txt:39,搜「gen_random_uuid()」)。 ↩ ↩2 -
出处:「What is an ORM and why use one?」第 21–31 段(
text/48-fm-what-is-an-orm-and-why-use-one.txt:53,搜「UC Berkeley」;:29,搜「PostGIS」)。 ↩ -
出处:「Prisma 7 architecture」第 5–31 段(
text/53-fm-prisma-7-architecture.txt:17,搜「TypeScript/WASM」;:23,搜「not consistently reproduced」;:27,搜「much smaller」)。 ↩ ↩2 ↩3 -
出处:「Prisma 7 architecture」第 33–41 段(
text/53-fm-prisma-7-architecture.txt:37,搜「driver adapters」;:39,搜「at runtime」)。 ↩ -
出处:「Anatomy of a Prisma Schema file」第 5–25 段(
text/58-fm-anatomy-of-a-prisma-schema-file.txt:62,搜「placed in your source tree」;:23,搜「P1012」)。 ↩ ↩2 -
出处:「Creating prisma.config.ts (new in Prisma 7)」第 11–19 段(
text/57-fm-creating-prisma-config-ts-new-in-prisma-7.txt:50,搜「automatically loaded in Prisma 7.」)。 ↩ -
出处:「Building the user model」第 15–39 段(
text/59-fm-building-the-user-model.txt:54,搜「native type annotation」;:35,搜「@updatedAt」;:39,搜「application level」)。 ↩ -
出处:「Adding the Post model (one-to-many)」第 29–55 段(
text/61-fm-adding-the-post-model-one-to-many.txt:60,搜「virtual」;:51,搜「does not automatically create」;:55,搜「full table scan」)。 ↩ ↩2 -
出处:「Adding Profile and Category models」第 35–45 段(
text/62-fm-adding-profile-and-category-models.txt:129,搜「constraint is what makes this a one-to-o」;:43,搜「at most one profile」)。 ↩ -
出处:「Adding Profile and Category models」第 47–57 段(
text/62-fm-adding-profile-and-category-models.txt:177,搜「implicit many-to-many」;:55,搜「native enum」)。 ↩ ↩2 -
出处:「Running our first migration」第 3–31 段(
text/60-fm-running-our-first-migration.txt:13,搜「two things」;:25,搜「SERIAL」)。 ↩ ↩2 -
出处:「What are migrations?」第 3–7 段(
text/64-fm-what-are-migrations.txt:9,搜「git-like history」)。 ↩ -
出处:「Key migration commands」第 3–21 段(
text/66-fm-key-migration-commands.txt:9,搜「workhorse」;:9,搜「production」;:13,搜「Never use this in production」;:17,搜「db push」)。 ↩ -
出处:「Customizing migrations」第 3–15 段(
text/67-fm-customizing-migrations.txt:23,搜「Never edit」;:15,搜「ledger」)。 ↩