---
title: "6.3 如何操作数据库"
description: "认识 ORM 工具，理解 CRUD 操作，掌握 AI 生成代码的审查要点"
---

# 6.3 如何操作数据库

> **本节目标**：理解数据库操作的核心概念（CRUD、ORM、事务），学会审查 AI 生成的数据库代码。

---

## 小红的第一个需求

上一节，小红设计好了校园外卖平台的表结构。表有了，关系也理清了，但数据库现在还是空的——就像盖好了一栋大楼，但里面一个人都没住进去。

现在她面临一个实际问题：怎么往数据库里存数据、取数据？

她的第一个需求很简单：用户注册。一个学生打开 App，填了手机号和昵称，点"注册"——后端需要把这条用户信息存进 `users` 表。

这个操作叫**插入（Insert）**，是数据库最基本的操作之一。听起来简单，但小红马上遇到了一个选择题：是直接写 SQL 语句，还是用别的方式？

她选择了用一个叫 **ORM** 的工具。

---

## ORM：用代码操作数据库的翻译官

**ORM（Object-Relational Mapping，对象关系映射）** 是一个"翻译层"，让你用编程语言（TypeScript/JavaScript）来操作数据库，而不需要手写 SQL。

你可以把 ORM 想象成一个翻译官：你说中文（TypeScript 代码），数据库只懂英文（SQL），ORM 负责在中间翻译。你写 `db.insert(users).values({ phone: '138xxxx', name: '小红' })`，ORM 翻译成 `INSERT INTO users (phone, name) VALUES ('138xxxx', '小红')` 发给数据库执行。

为什么需要这个翻译官？直接写 SQL 不行吗？

**直接写 SQL 的问题。** SQL 是数据库的"母语"，功能强大但有几个痛点：字符串拼接容易出错、没有类型检查（写错列名要到运行时才发现）、不同数据库的 SQL 语法有差异。对于 Vibe Coder 来说，最大的问题是——你让 AI 生成 SQL，很难一眼看出对不对。一条复杂的 SQL 可能有十几行，嵌套三四层，光是括号匹配就让人头晕。

**ORM 的好处。** ORM 把数据库表映射成代码里的对象，把 SQL 操作映射成函数调用。列名有自动补全、类型有检查、写错了编辑器直接报红。而且 ORM 生成的代码更容易阅读和审查——你看到 `db.select().from(users).where(eq(users.phone, '138xxxx'))`，即使不懂 SQL 也能猜到它在做什么。

本教程推荐 **Drizzle ORM**，原因是：

- 语法贴近 SQL，学过 SQL 概念就能看懂
- TypeScript 类型安全，AI 生成的代码更容易审查
- 同时支持 PostgreSQL 和 SQLite，方便切换
- 社区活跃，文档完善

另一个流行的选择是 **Prisma**，它用自己的 Schema 语言定义表结构，语法更简洁但离 SQL 更远。两者都是好工具，选一个坚持用就行。重要的不是选哪个，而是理解 ORM 背后的概念——因为概念是通用的，工具可以换。

---

## CRUD：数据库操作的四个基本动作

不管你做的是外卖平台、社交应用、电商网站还是博客系统，所有数据库操作归根结底就四种，合称 **CRUD**：

| 操作 | 英文 | 含义 | 生活类比 |
|------|------|------|---------|
| **C**reate | 创建 | 往表里插入新数据 | 新用户注册、下新订单 |
| **R**ead | 读取 | 从表里查询数据 | 查看订单列表、搜索商品 |
| **U**pdate | 更新 | 修改已有数据 | 改收货地址、更新订单状态 |
| **D**elete | 删除 | 移除数据 | 取消订单、注销账号 |

CRUD 这个缩写在开发圈子里非常常见。当有人说"这个功能就是个 CRUD"，意思是"这个功能的核心就是对数据库做增删改查，没有复杂的业务逻辑"。事实上，大部分 Web 应用的 80% 功能都是 CRUD——真正复杂的业务逻辑只占很小一部分。

这四个操作覆盖了 99% 的业务场景。你在任何应用里看到的功能，拆到数据库层面都是 CRUD 的组合。

比如小红的外卖平台：

- 用户注册 → **Create** 一条 users 记录
- 浏览菜单 → **Read** dishes 表，按商家筛选
- 下单 → **Create** 一条 orders 记录 + 多条 order_items 记录
- 改地址 → **Update** users 表的 address 字段
- 取消订单 → **Update** orders 的 status 为"已取消"（通常不真删）
- 商家下架菜品 → **Delete** 或 **Update** dishes 的状态

注意最后一条：实际项目中很少真的删除数据。更常见的做法是**"软删除"**——加一个 `deleted_at` 字段，标记为已删除但数据还在。这样万一删错了还能恢复，也方便做数据分析。

为什么不直接删？想象一下：用户取消了订单，你把订单记录从数据库里彻底删掉了。一个月后用户来投诉说"我明明下过单，你们怎么没记录？"你拿什么证明？软删除就是给数据加一个"回收站"——标记为已删除，但数据还在，需要的时候随时可以恢复或查证。

<CRUDVisualizer mode="crud" />

---

## 审查 AI 生成的 CRUD 代码

作为 Vibe Coder，你不需要自己写 CRUD 代码——AI 会帮你写。但你需要知道怎么审查 AI 生成的代码。

这就像你请了一个装修工人。你不需要自己刷墙、铺地板，但你得知道墙刷得平不平、地板铺得正不正。如果你完全不懂装修，工人偷工减料你也看不出来。

AI 生成的数据库代码也是一样。它大部分时候是对的，但偶尔会犯一些"看起来能跑但其实有隐患"的错误。以下是每种操作的审查要点：

**Create（插入）审查清单：**

- 必填字段都传了吗？（NOT NULL 的列不能漏）
- 有没有做输入校验？（手机号格式、昵称长度）
- 插入失败时有没有错误处理？（比如手机号重复，数据库会报 UNIQUE 约束冲突）

**Read（查询）审查清单：**

- 查询条件对吗？（是按 `user_id` 查还是按 `phone` 查）
- 需要关联查询吗？（查订单时要不要带上用户信息）
- 有没有分页？（不加分页，数据量大了会一次返回几万条，直接把页面卡死）
- 有没有排序？（通常按创建时间倒序，最新的在前面）
- **有没有 N+1 问题？**（见下方说明）

**Update（更新）审查清单：**

- WHERE 条件对吗？（没有 WHERE 会更新整张表的所有行！）
- 只更新了需要改的字段吗？（不要把其他字段也覆盖了）
- 有没有更新 `updated_at` 时间戳？

**Delete（删除）审查清单：**

- 是真删除还是软删除？（生产环境建议软删除）
- WHERE 条件对吗？（没有 WHERE 会删掉整张表！）
- 有没有考虑关联数据？（删用户前，他的订单怎么办？）

::: danger WHERE 条件是生命线
Update 和 Delete 操作如果忘了写 WHERE 条件，会影响表里的**所有行**。这是数据库操作中最常见也最致命的错误。

想象你想把小红的地址改成"3 号楼 201"，结果 AI 生成的代码忘了加 WHERE 条件——整张 `users` 表里所有用户的地址都被改成了"3 号楼 201"。几千个用户的地址，一瞬间全变了。

审查 AI 代码时，看到 UPDATE 或 DELETE，第一件事就是检查有没有 WHERE。没有 WHERE 的 UPDATE/DELETE 几乎永远是 bug。
:::

---

## 常见性能陷阱

代码能跑和跑得快是两回事。以下三个性能陷阱在实际项目中最常见——不管是人写的还是 AI 写的，数据量一大都可能踩到。好消息是，你只需要知道这些坑的名字，遇到性能问题时告诉 AI "检查一下有没有 N+1 查询"就行。

### N+1 查询：最隐蔽的性能杀手

小红要做一个"商家后台"页面，展示所有用户和他们各自的订单数量。

一种常见的错误写法是：先查出所有用户（1 次查询），然后**对每个用户分别查他的订单**（N 次查询）。如果有 100 个用户，就是 101 次数据库查询——这就是 N+1 问题。

为什么叫 N+1？因为第一次查询返回了 N 条记录，然后对每条记录又发了 1 次查询，总共 N+1 次。

这个问题特别隐蔽，因为在开发环境里（数据少、网络延迟低）几乎感觉不到。但到了生产环境，每次数据库查询都有网络往返的开销（通常 1-5 毫秒）。100 次查询就是 100-500 毫秒，再加上数据库本身的处理时间，页面加载轻松突破好几秒。

正确的做法是用 JOIN 或批量查询，一次把所有数据取出来。审查时如果看到"循环里套着数据库查询"，基本就是 N+1。

N+1 能让页面加载从 100ms 变成 10 秒。如果你发现某个页面莫名其妙地慢，第一个怀疑对象就是它。

<CRUDVisualizer mode="n1" />

### 分页越翻越慢：OFFSET 的陷阱

小红的菜品列表有上千道菜，需要分页展示——每页 20 道，用户可以翻页浏览。AI 默认生成的分页通常是 OFFSET 方式——"跳过前 N 条，取接下来的 20 条"。

前几页很快，但翻到第 50 页时明显变慢了。为什么？

想象你在图书馆找第 1000 本书。OFFSET 的做法是：从第一本开始数，数到第 1000 本，然后取出后面 20 本。每次翻页都要从头数起，页数越深，数的越多，越慢。

数据库也是这样。`OFFSET 1000 LIMIT 20` 意味着数据库要先扫描前面 1000 条被跳过的行，然后才取出你要的 20 条。这些被跳过的行虽然不返回给你，但数据库还是要读取和处理它们。

更好的方式是**游标分页（Cursor Pagination）**：记住上一页最后一条记录的 ID，下一页直接从那个 ID 之后开始取。就像在图书馆里放了一个书签，下次直接从书签位置开始，不需要从头数。无论翻到第几页，速度都一样快。

审查分页代码时，看到 `OFFSET` 就要留个心眼——数据量小无所谓，数据量大了一定要换成游标分页。

<CRUDVisualizer mode="paging" />

### 先查再改的并发陷阱

小红要做一个"用户设置"功能。用户修改主题颜色时，设置可能已经存在（需要更新），也可能是第一次设置（需要插入）。

AI 可能生成"先查有没有，有就更新，没有就插入"的逻辑。这在只有一个用户的时候没问题。但想象这个场景：用户快速连点了两次"保存"按钮，两个请求几乎同时到达服务器。

第一个请求查了一下："设置不存在"。第二个请求也查了一下："设置不存在"。然后两个请求都去执行插入操作——第二个就会因为主键或唯一约束冲突而报错。

这个问题叫**竞态条件**，在 6.1 节讲 JSON 并发写入时已经提过。数据库有一个专门解决这个问题的操作叫 **UPSERT**（INSERT ... ON CONFLICT DO UPDATE）——一条语句原子完成"有就更新、没有就插入"，不会有并发冲突。

审查时看到"先 SELECT 再决定 INSERT 还是 UPDATE"的模式，就应该改成 UPSERT。

---

## 事务：要么全成功，要么全失败

小红的平台上，用户下单时需要同时做三件事：

1. 创建一条订单记录
2. 创建多条订单明细记录（点了哪些菜）
3. 扣减对应菜品的库存

如果第 1 步成功了，第 2 步也成功了，但第 3 步扣库存时发现库存不足——怎么办？订单已经创建了，但菜品没库存了，这就是**数据不一致**。用户以为下单成功了，商家一看库存已经是负数了，两边都懵。

**事务（Transaction）** 就是解决这个问题的。事务把多个操作打包成一个"原子操作"：要么全部成功，要么全部回滚（撤销），不会出现"做了一半"的中间状态。

"原子"这个词来自物理学——原子是不可再分的最小单位。事务也是一样，它是数据库操作的最小单位，不存在"执行了一半"的状态。

用一个更生活化的例子来理解：你去 ATM 转账，从 A 账户转 100 块到 B 账户。这个操作包含两步：A 扣 100、B 加 100。如果 A 扣了钱但 B 没加上（比如系统崩溃了），那 100 块就凭空消失了。事务保证这两步要么都完成，要么都不做——如果中途出错，A 扣的钱会自动退回来。

事务的四个特性（ACID）：

| 特性 | 含义 | 小红的例子 |
|------|------|-----------|
| **A**tomicity 原子性 | 全成功或全失败 | 下单三步要么都完成，要么都撤销 |
| **C**onsistency 一致性 | 操作前后数据都合法 | 库存不会变成负数 |
| **I**solation 隔离性 | 并发操作互不干扰 | 两人同时抢最后一份，只有一人成功 |
| **D**urability 持久性 | 成功后数据永久保存 | 服务器重启，订单数据还在 |

<CRUDVisualizer mode="tx" />

你不需要自己实现事务逻辑——AI 会用 ORM 的事务 API 帮你包装好。你只需要审查：哪些操作应该放在同一个事务里。

判断标准很简单：**如果几个操作必须同时成功或同时失败，就放在一个事务里。**

常见需要事务的场景：

- 下单（创建订单 + 扣库存）
- 转账（A 扣钱 + B 加钱）
- 注册（创建用户 + 创建默认配置 + 发送欢迎消息）
- 发帖（创建帖子 + 更新用户发帖计数 + 创建动态记录）

::: warning 事务要短小精悍
事务执行期间会持有数据库锁，阻塞其他操作。如果事务里包含了调用外部 API、发送邮件等耗时操作，锁会被长时间占用，导致其他用户的请求排队等待。

想象你在银行柜台办业务，柜员帮你转完账后，又帮你打电话确认、发短信通知、打印回执……后面排队的人全在等。正确的做法是：柜员只做转账这一件事（数据库操作），转完账你就离开柜台，打电话、发短信这些事在旁边自己办（事务之外）。

原则：**事务里只放数据库操作**，外部调用放在事务之外。比如"创建订单 + 发通知"，应该先在事务里创建订单，事务提交后再发通知。
:::

---

## Schema 定义：数据库的蓝图

在让 AI 写 CRUD 之前，你需要先定义好表结构（Schema）。Schema 就是数据库的"蓝图"，描述了有哪些表、每张表有哪些列、列的类型和约束是什么。

你可以把 Schema 想象成建筑图纸。盖房子之前，建筑师要画好图纸：几层楼、每层几个房间、每个房间多大、门窗在哪里。数据库也一样——先画好"图纸"（Schema），再往里面"住人"（存数据）。

Schema 不是你手写的——AI 会根据你的业务需求自动生成。但你需要能看懂它，知道它设计得对不对。

AI 生成 Schema 后，你需要审查这些要点：

- **主键设置正确吗？** 每张表都应该有 `id` 主键
- **外键方向对吗？** `posts.userId` 应该指向 `users.id`，不能反了。外键永远在"多"的那一方——一个用户有多篇帖子，所以 `userId` 在 `posts` 表里，不在 `users` 表里
- **该加的约束加了吗？** 点赞表的 `userId + postId` 应该有联合唯一约束——"这两个字段的组合不能重复"，也就是同一个用户只能给同一篇帖子点一次赞。没有这个约束，用户连点两次就会产生两条点赞记录
- **字段类型合适吗？** 帖子内容用 `text`（不限长度），不要用 `varchar(255)`——6.2 节讲过，PostgreSQL 里两者性能一样，`varchar` 只是多了个不必要的长度限制
- **时间戳齐全吗？** `created_at` 和 `updated_at` 是标配，几乎每张表都应该有
- **索引考虑了吗？** 经常用来查询的字段（如 `userId`、`postId`）应该有索引——6.4 节会详细讲

---

## 数据库迁移：Schema 变更的版本管理

表结构不是一成不变的。随着功能迭代，你可能需要给 `users` 表加一个 `avatar` 字段，或者给 `posts` 表加一个 `is_pinned` 列。

直接改数据库？太危险了。万一改错了，线上数据就毁了。而且你怎么记得住自己改了什么？三个月后回头看，完全想不起来当初为什么加了这个列。

**迁移（Migration）** 是数据库的"版本管理"，就像 Git 管理代码版本一样。每次修改表结构，都生成一个迁移文件，记录"做了什么改动"。这样可以：

- **追踪历史**：知道表结构是怎么一步步变成现在这样的，每次改动都有记录
- **团队协作**：队友拉代码后执行迁移，数据库结构自动同步，不需要手动对齐
- **安全回滚**：改错了可以回退到上一个版本，就像 Git 的 `revert`

Drizzle ORM 有内置的迁移工具。工作流程是这样的：你修改 Schema 代码（比如给 `users` 表加一个 `avatar` 列），然后运行迁移命令，工具会自动对比新旧 Schema 的差异，生成一个迁移 SQL 文件（比如 `0001_add_avatar_to_users.sql`），最后执行这个文件来更新数据库。

::: warning 生产环境迁移要谨慎
开发环境随便改，但生产环境的迁移要小心。有些操作是**不可逆的**：删掉一个列，里面的数据就永远没了；把 `text` 改成 `integer`，不符合格式的数据会丢失。

AI 生成迁移后，一定要先在开发环境测试，确认无误再应用到生产。特别是涉及删列、改类型的操作，要格外谨慎。如果不确定，先备份。
:::

---

## 工作流总结

作为 Vibe Coder，你和数据库打交道的工作流是这样的：

1. **设计阶段**：把你的业务需求描述清楚，AI 会生成 Schema
2. **审查 Schema**：检查表结构、关系、约束是否正确（用 6.2 学到的知识）
3. **生成 CRUD**：AI 根据 Schema 生成增删改查代码
4. **审查 CRUD**：重点检查 WHERE 条件、事务使用、错误处理（用本节学到的知识）
5. **迭代修改**：需求变了，AI 修改 Schema 并生成迁移
6. **测试验证**：在开发环境跑通，再部署到生产

注意你在这个流程里的角色：**你不是"写数据库代码的人"，而是"审查数据库代码的人"。** AI 负责写，你负责判断对不对。

这就是为什么前面两节花了大量篇幅讲概念——只有理解了主键、外键、约束、关系类型，你才能有效地审查 AI 的输出。不理解这些概念，你就只能"AI 说什么就是什么"，遇到问题也不知道从哪里下手排查。

---

::: tip 本节核心要点

- **ORM** 是代码和数据库之间的翻译层，推荐 Drizzle
- **CRUD** 是所有数据库操作的基础：创建、读取、更新、删除
- **事务** 保证多个操作要么全成功要么全失败
- **审查重点**：WHERE 条件、外键方向、唯一约束、事务边界
- **迁移** 是表结构的版本管理，改表结构必须走迁移流程
:::
