6.3 如何操作数据库 转载

来源:https://github.com/datawhalechina/vibe-vibe

本节目标:理解数据库操作的核心概念(CRUD、ORM、事务),学会审查 AI 生成的数据库代码。

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 背后的概念——因为概念是通用的,工具可以换。

审查 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 会删掉整张表!)
  • 有没有考虑关联数据?(删用户前,他的订单怎么办?)
🚫 WHERE 条件是生命线

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

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

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

事务:要么全成功,要么全失败

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

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

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

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

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

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

事务的四个特性(ACID):

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

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

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

常见需要事务的场景:

  • 下单(创建订单 + 扣库存)
  • 转账(A 扣钱 + B 加钱)
  • 注册(创建用户 + 创建默认配置 + 发送欢迎消息)
  • 发帖(创建帖子 + 更新用户发帖计数 + 创建动态记录)
⚠️ 事务要短小精悍

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

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

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

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

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

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

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

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

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

⚠️ 生产环境迁移要谨慎

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

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

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