13 分钟
全栈开发

数据库迁移与 ORM:Schema 怎么演进

迁移文件版本化、ORM 映射对象、参数化查询防注入

  • 理解为什么不能手改生产库
  • 知道迁移文件是版本化的改表日志
  • 认识 Prisma/Drizzle/Alembic 是什么
  • 理解 ORM 把表映射成对象、参数化查询防注入
💡高中搭桥

数据库迁移就像 Git 的提交记录:每次改表结构都写成一个带编号的文件,按顺序执行、留痕、可回滚。团队每人本地的库、测试库、线上库,都靠同一套迁移脚本演变出一致的结构,不会谁手动改出差异。

为什么不能手改生产库?

你在自己电脑上 ALTER TABLE 加了一列,团队其他人不知道,部署到线上又没人执行,结果代码和表结构对不上,直接报错。手改还没法回滚、没法多人协作。正确做法:每次改表都生成一个迁移文件,纳入 Git,部署时自动按顺序跑。

再往深说三层。一是多人协作:两个人各改各的本地库,谁也说不清线上到底该长什么样,新同事拉代码下来,表结构对不上根本跑不起来。二是回滚:手改错了想退回去?没有记录就只能凭记忆再写一条反向 SQL,生产数据已经脏了。三是审计:半年后出问题,没人记得是谁、什么时候、为什么加了这一列,线上数据库就是一笔糊涂账。迁移文件把每一次变更都变成 Git 里可 review、可追溯、可回滚的一条记录,团队所有人的库都从同一份记录确定性地长出来。

迁移文件 = 版本化的改表日志

每个迁移文件只做一件事(建表、加列、加索引),带递增编号,数据库记录「已经执行到哪一步」。新环境从头跑到最新,老环境只跑没执行过的。文件命名通常带时间戳或序号,保证多人同时提交迁移时也能排定先后顺序,不会互相覆盖。回滚时也有对应方向:把编号倒着跑回去,结构就能退到上一版。

SQL
# 迁移文件长这样(示意)
# 001_create_users.sql
CREATE TABLE users (id INT, name TEXT);

# 002_add_email.sql
ALTER TABLE users ADD COLUMN email TEXT;

# 003_create_posts.sql
CREATE TABLE posts (id INT, title TEXT, user_id INT);
ℹ️主流工具

Node 生态常用 Prisma、Drizzle ORM;Python 生态用 Alembic(配 SQLAlchemy)。它们自动生成迁移文件、记录已执行版本、支持回滚。具体版本号与用法以官方最新文档为准。

动手:手写最简迁移脚本

下面模拟迁移系统:按编号顺序执行每一步,打印执行日志,所有环境跑出来的结构一致。

示例代码(可运行)

ORM 与参数化查询

ORM(对象关系映射)把数据库表映射成代码里的对象:你用 user.name 读写,而不是拼 SQL 字符串。它还自动做参数化查询——把用户输入当数据传进去,而不是拼进 SQL 字符串里,从根上防住 SQL 注入。

SQL
// 危险:字符串拼接,SQL 注入漏洞
const bad = "SELECT * FROM users WHERE name = '" + input + "'";

// 安全:ORM / 参数化查询,输入永远被当数据
const good = await db.user.findMany({ where: { name: input } });
🚫SQL 注入真实演示

假设登录查询是拼出来的:SELECT * FROM users WHERE name = 'input'。攻击者在输入框填 ' OR '1'='1,拼出来就变成 WHERE name = '' OR '1'='1'——条件恒真,直接绕过登录拿到所有数据。更狠的还能拼出删表语句。参数化查询把 input 当成纯数据传给数据库,数据库知道它只是一个字符串值,绝不会把它当 SQL 指令执行,注入从根上失效。

ORM 的坑:lazy loading 引发 N+1

ORM 让你写 user.posts 就像访问属性,很爽,但它背后可能偷偷发查询。这叫懒加载(lazy loading):你第一次访问 user.posts 时,ORM 才去数据库查这个用户的文章。问题来了——如果你循环取 50 个用户,每个都访问一次 user.posts,ORM 就老老实实发了 50 次查询,正是上一课讲的 N+1。ORM 帮你防注入,却也可能悄悄制造 N+1。解法是用 ORM 的「预加载」(Prisma 的 include、SQLAlchemy 的 joinedload):在主查询时一次性把关联数据 JOIN 或批量取回来,而不是访问时才逐条查。

⚠️别以为用了 ORM 就快

ORM 是把双刃剑:它挡注入、提生产力,但默认的懒加载写法很容易写出 N+1。上线前用日志或慢查询统计看一眼实际发了几条 SQL,别被「代码很优雅」骗了。

⚠️大表迁移要小心

迁移文件虽然版本化了,但在几百万行的大表上直接 ALTER TABLE 加列,可能锁表几十秒甚至几分钟,线上服务直接卡住。真实生产里要拆步骤、用在线变更工具(如同时维护新旧两列、双写、再切流),而不是一条 ALTER 顶上去。入门先记住:迁移不只是「写个 SQL 跑了」,大表的迁移策略本身是个工程问题。

选择题

为什么不能直接手动改生产数据库的表结构?

填空题填写空白处的代码
ORM 把数据库表映射成代码里的 ,不用手写 SQL 字符串拼接。

资深工程师加餐

底层原理 · 大厂视角 · 工程经验,点卡片展开

穿透是反复查「根本不存在」的数据,用布隆过滤器或缓存空值挡;击穿是某个热点 key 过期瞬间大量请求压向数据库,用互斥重建或逻辑过期;雪崩是大批 key 同时过期,给 TTL 加随机抖动。缓存与库的一致性常用「先更新数据库、再删除缓存」,并靠重试/订阅补偿。

挑战任务

按顺序跑迁移

简单+50 XP

迁移系统按编号顺序执行每个迁移并记录。实现 run(migs):migs 是 (名称, SQL) 列表,按顺序收集已执行的名称返回。

按顺序跑迁移
1 个测试用例

课后作业

用参数化查询挡住 SQL 注入

中等+15 XP

把用户输入拼进 SQL 字符串会被注入。参数化查询用 ? 占位符,输入永远作为独立参数传进去。实现 safe_query(name):返回 (SQL语句, 参数元组),SQL 里写死 WHERE name = ?,name 放进参数元组。打印面对恶意输入时的结果。

用参数化查询挡住 SQL 注入
1 个测试用例