title: "6.3 如何操作数据库" description: "认识 ORM 工具,理解 CRUD 操作,掌握 AI 生成代码的审查要点" 6.3 如何操作数据库 本节目标:理解数据库操作的核心概念(CRUD、ORM、事务),学会审查 AI 生成的数据库代码。 小红的第一个需求 上一节,小红设计好了校园外卖平台的表结构。表有了,关系也理清了,但数据库现在还是空的——就像盖好了一栋大楼,但里面一个人都没住进去。 现在她面临一个实际问题:怎么往数据库里存数据、取数据? 她的第一个需求很简单:用户注册。一个学生打开 App,填了手机号和昵称,点"注册"——后端需要把这条用户信息存进 表。 这个操作叫插入(Insert),是数据库最基本的操作之一。
title: "6.3 如何操作数据库" description: "认识 ORM 工具,理解 CRUD 操作,掌握 AI 生成代码的审查要点"
本节目标:理解数据库操作的核心概念(CRUD、ORM、事务),学会审查 AI 生成的数据库代码。
上一节,小红设计好了校园外卖平台的表结构。表有了,关系也理清了,但数据库现在还是空的——就像盖好了一栋大楼,但里面一个人都没住进去。
现在她面临一个实际问题:怎么往数据库里存数据、取数据?
她的第一个需求很简单:用户注册。一个学生打开 App,填了手机号和昵称,点"注册"——后端需要把这条用户信息存进 users 表。
这个操作叫插入(Insert),是数据库最基本的操作之一。听起来简单,但小红马上遇到了一个选择题:是直接写 SQL 语句,还是用别的方式?
她选择了用一个叫 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,原因是:
另一个流行的选择是 Prisma,它用自己的 Schema 语言定义表结构,语法更简洁但离 SQL 更远。两者都是好工具,选一个坚持用就行。重要的不是选哪个,而是理解 ORM 背后的概念——因为概念是通用的,工具可以换。
不管你做的是外卖平台、社交应用、电商网站还是博客系统,所有数据库操作归根结底就四种,合称 CRUD:
| 操作 | 英文 | 含义 | 生活类比 |
|---|---|---|---|
| Create | 创建 | 往表里插入新数据 | 新用户注册、下新订单 |
| Read | 读取 | 从表里查询数据 | 查看订单列表、搜索商品 |
| Update | 更新 | 修改已有数据 | 改收货地址、更新订单状态 |
| Delete | 删除 | 移除数据 | 取消订单、注销账号 |
CRUD 这个缩写在开发圈子里非常常见。当有人说"这个功能就是个 CRUD",意思是"这个功能的核心就是对数据库做增删改查,没有复杂的业务逻辑"。事实上,大部分 Web 应用的 80% 功能都是 CRUD——真正复杂的业务逻辑只占很小一部分。
这四个操作覆盖了 99% 的业务场景。你在任何应用里看到的功能,拆到数据库层面都是 CRUD 的组合。
比如小红的外卖平台:
注意最后一条:实际项目中很少真的删除数据。更常见的做法是**"软删除"**——加一个 deleted_at 字段,标记为已删除但数据还在。这样万一删错了还能恢复,也方便做数据分析。
为什么不直接删?想象一下:用户取消了订单,你把订单记录从数据库里彻底删掉了。一个月后用户来投诉说"我明明下过单,你们怎么没记录?"你拿什么证明?软删除就是给数据加一个"回收站"——标记为已删除,但数据还在,需要的时候随时可以恢复或查证。
作为 Vibe Coder,你不需要自己写 CRUD 代码——AI 会帮你写。但你需要知道怎么审查 AI 生成的代码。
这就像你请了一个装修工人。你不需要自己刷墙、铺地板,但你得知道墙刷得平不平、地板铺得正不正。如果你完全不懂装修,工人偷工减料你也看不出来。
AI 生成的数据库代码也是一样。它大部分时候是对的,但偶尔会犯一些"看起来能跑但其实有隐患"的错误。以下是每种操作的审查要点:
Create(插入)审查清单:
Read(查询)审查清单:
user_id 查还是按 phone 查)Update(更新)审查清单:
updated_at 时间戳?Delete(删除)审查清单:
::: danger WHERE 条件是生命线
Update 和 Delete 操作如果忘了写 WHERE 条件,会影响表里的所有行。这是数据库操作中最常见也最致命的错误。
想象你想把小红的地址改成"3 号楼 201",结果 AI 生成的代码忘了加 WHERE 条件——整张 users 表里所有用户的地址都被改成了"3 号楼 201"。几千个用户的地址,一瞬间全变了。
审查 AI 代码时,看到 UPDATE 或 DELETE,第一件事就是检查有没有 WHERE。没有 WHERE 的 UPDATE/DELETE 几乎永远是 bug。
:::
代码能跑和跑得快是两回事。以下三个性能陷阱在实际项目中最常见——不管是人写的还是 AI 写的,数据量一大都可能踩到。好消息是,你只需要知道这些坑的名字,遇到性能问题时告诉 AI "检查一下有没有 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 秒。如果你发现某个页面莫名其妙地慢,第一个怀疑对象就是它。
小红的菜品列表有上千道菜,需要分页展示——每页 20 道,用户可以翻页浏览。AI 默认生成的分页通常是 OFFSET 方式——"跳过前 N 条,取接下来的 20 条"。
前几页很快,但翻到第 50 页时明显变慢了。为什么?
想象你在图书馆找第 1000 本书。OFFSET 的做法是:从第一本开始数,数到第 1000 本,然后取出后面 20 本。每次翻页都要从头数起,页数越深,数的越多,越慢。
数据库也是这样。OFFSET 1000 LIMIT 20 意味着数据库要先扫描前面 1000 条被跳过的行,然后才取出你要的 20 条。这些被跳过的行虽然不返回给你,但数据库还是要读取和处理它们。
更好的方式是游标分页(Cursor Pagination):记住上一页最后一条记录的 ID,下一页直接从那个 ID 之后开始取。就像在图书馆里放了一个书签,下次直接从书签位置开始,不需要从头数。无论翻到第几页,速度都一样快。
审查分页代码时,看到 OFFSET 就要留个心眼——数据量小无所谓,数据量大了一定要换成游标分页。
小红要做一个"用户设置"功能。用户修改主题颜色时,设置可能已经存在(需要更新),也可能是第一次设置(需要插入)。
AI 可能生成"先查有没有,有就更新,没有就插入"的逻辑。这在只有一个用户的时候没问题。但想象这个场景:用户快速连点了两次"保存"按钮,两个请求几乎同时到达服务器。
第一个请求查了一下:"设置不存在"。第二个请求也查了一下:"设置不存在"。然后两个请求都去执行插入操作——第二个就会因为主键或唯一约束冲突而报错。
这个问题叫竞态条件,在 6.1 节讲 JSON 并发写入时已经提过。数据库有一个专门解决这个问题的操作叫 UPSERT(INSERT ... ON CONFLICT DO UPDATE)——一条语句原子完成"有就更新、没有就插入",不会有并发冲突。
审查时看到"先 SELECT 再决定 INSERT 还是 UPDATE"的模式,就应该改成 UPSERT。
小红的平台上,用户下单时需要同时做三件事:
如果第 1 步成功了,第 2 步也成功了,但第 3 步扣库存时发现库存不足——怎么办?订单已经创建了,但菜品没库存了,这就是数据不一致。用户以为下单成功了,商家一看库存已经是负数了,两边都懵。
事务(Transaction) 就是解决这个问题的。事务把多个操作打包成一个"原子操作":要么全部成功,要么全部回滚(撤销),不会出现"做了一半"的中间状态。
"原子"这个词来自物理学——原子是不可再分的最小单位。事务也是一样,它是数据库操作的最小单位,不存在"执行了一半"的状态。
用一个更生活化的例子来理解:你去 ATM 转账,从 A 账户转 100 块到 B 账户。这个操作包含两步:A 扣 100、B 加 100。如果 A 扣了钱但 B 没加上(比如系统崩溃了),那 100 块就凭空消失了。事务保证这两步要么都完成,要么都不做——如果中途出错,A 扣的钱会自动退回来。
事务的四个特性(ACID):
| 特性 | 含义 | 小红的例子 |
|---|---|---|
| Atomicity 原子性 | 全成功或全失败 | 下单三步要么都完成,要么都撤销 |
| Consistency 一致性 | 操作前后数据都合法 | 库存不会变成负数 |
| Isolation 隔离性 | 并发操作互不干扰 | 两人同时抢最后一份,只有一人成功 |
| Durability 持久性 | 成功后数据永久保存 | 服务器重启,订单数据还在 |
你不需要自己实现事务逻辑——AI 会用 ORM 的事务 API 帮你包装好。你只需要审查:哪些操作应该放在同一个事务里。
判断标准很简单:如果几个操作必须同时成功或同时失败,就放在一个事务里。
常见需要事务的场景:
::: warning 事务要短小精悍
事务执行期间会持有数据库锁,阻塞其他操作。如果事务里包含了调用外部 API、发送邮件等耗时操作,锁会被长时间占用,导致其他用户的请求排队等待。
想象你在银行柜台办业务,柜员帮你转完账后,又帮你打电话确认、发短信通知、打印回执……后面排队的人全在等。正确的做法是:柜员只做转账这一件事(数据库操作),转完账你就离开柜台,打电话、发短信这些事在旁边自己办(事务之外)。
原则:事务里只放数据库操作,外部调用放在事务之外。比如"创建订单 + 发通知",应该先在事务里创建订单,事务提交后再发通知。
:::
在让 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 节会详细讲表结构不是一成不变的。随着功能迭代,你可能需要给 users 表加一个 avatar 字段,或者给 posts 表加一个 is_pinned 列。
直接改数据库?太危险了。万一改错了,线上数据就毁了。而且你怎么记得住自己改了什么?三个月后回头看,完全想不起来当初为什么加了这个列。
迁移(Migration) 是数据库的"版本管理",就像 Git 管理代码版本一样。每次修改表结构,都生成一个迁移文件,记录"做了什么改动"。这样可以:
revertDrizzle ORM 有内置的迁移工具。工作流程是这样的:你修改 Schema 代码(比如给 users 表加一个 avatar 列),然后运行迁移命令,工具会自动对比新旧 Schema 的差异,生成一个迁移 SQL 文件(比如 0001_add_avatar_to_users.sql),最后执行这个文件来更新数据库。
::: warning 生产环境迁移要谨慎
开发环境随便改,但生产环境的迁移要小心。有些操作是不可逆的:删掉一个列,里面的数据就永远没了;把 text 改成 integer,不符合格式的数据会丢失。
AI 生成迁移后,一定要先在开发环境测试,确认无误再应用到生产。特别是涉及删列、改类型的操作,要格外谨慎。如果不确定,先备份。
:::
作为 Vibe Coder,你和数据库打交道的工作流是这样的:
注意你在这个流程里的角色:你不是"写数据库代码的人",而是"审查数据库代码的人"。 AI 负责写,你负责判断对不对。
这就是为什么前面两节花了大量篇幅讲概念——只有理解了主键、外键、约束、关系类型,你才能有效地审查 AI 的输出。不理解这些概念,你就只能"AI 说什么就是什么",遇到问题也不知道从哪里下手排查。
::: tip 本节核心要点