迁移脚本管理
要点
- 数据库迁移是把 schema 变更应用到数据库的过程,需要版本化、可重复、可回滚
- Drizzle Kit 和 Prisma Migrate 是两种主流的迁移工具
- 迁移文件应该纳入版本控制,便于协作和审计
- 生产环境的迁移应该通过 CI/CD 自动执行,避免手动操作
- 回滚策略需要提前规划,部分迁移不可逆(例如删除列、删除数据)
- 大表的 schema 变更需要考虑锁表时间和在线迁移方案
1. 迁移的概念
数据库迁移(migration)是把 schema 变更(增删表、修改列、添加索引等)以脚本的形式管理,使得:
- 变更可重复执行(任何环境都能得到一致的数据库状态)
- 变更可追溯(通过迁移文件的历史可以看到数据库的演进)
- 变更可协作(团队成员可以共享迁移,CI/CD 可以自动应用)
2. Drizzle Kit 迁移
2.1 配置
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
schema: './src/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
})2.2 生成迁移
# 对比 schema 和当前数据库状态,生成迁移文件
pnpm drizzle-kit generate生成的迁移文件存放在 drizzle/ 目录:
drizzle/
├── 0001_create_users.sql
├── 0002_add_conversations.sql
├── 0003_add_message_count.sql
├── meta/
│ ├── _journal.json
│ └── 0001_snapshot.json
└── ...每个迁移文件包含 SQL 语句:
-- drizzle/0001_create_users.sql
CREATE TABLE IF NOT EXISTS "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"email" text NOT NULL,
"name" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "users_email_unique" UNIQUE("email")
);2.3 应用迁移
# 应用所有待执行的迁移
pnpm drizzle-kit migrate
# 开发环境直接推送(不生成迁移文件)
pnpm drizzle-kit push
# 查看迁移状态
pnpm drizzle-kit status2.4 回滚
Drizzle Kit 不直接支持回滚(因为 SQL 回滚复杂)。需要手动编写回滚 SQL:
-- drizzle/0001_create_users.rollback.sql
DROP TABLE IF EXISTS "users";或者通过版本控制还原到之前的状态。
3. Prisma Migrate
3.1 配置
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}3.2 生成并应用迁移
# 开发环境:生成并应用迁移(交互式)
pnpx prisma migrate dev --name init
# 生产环境:应用已有的迁移
pnpx prisma migrate deploy
# 重置数据库(删除所有数据)
pnpx prisma migrate reset
# 查看迁移状态
pnpx prisma migrate status迁移文件存放在 prisma/migrations/ 目录:
prisma/migrations/
├── 20260620120000_init/
│ └── migration.sql
├── 20260620130000_add_conversations/
│ └── migration.sql
└── migration_lock.toml3.3 回滚
Prisma Migrate 同样不直接支持回滚。需要:
- 编写新的迁移来撤销变更
- 或者使用
prisma migrate reset重置数据库(仅开发环境)
3.4 基线迁移
对于已有数据库,可以创建基线迁移:
# 标记当前数据库状态为基线
pnpx prisma migrate diff --from-empty --to-schema-datamodel prisma/schema.prisma --script > prisma/migrations/0_init/migration.sql
# 应用基线(标记为已执行)
pnpx prisma migrate resolve --applied 0_init4. 迁移与版本控制
迁移文件应该纳入版本控制(git),这样可以:
- 团队协作:其他成员拉取代码后可以执行迁移
- 审计:可以看到数据库的演进历史
- 环境一致性:staging 和 production 可以应用相同的迁移
# 提交迁移文件
git add drizzle/ # 或 prisma/migrations/
git commit -m "feat(db): add user conversations table"5. CI/CD 集成
生产环境的迁移应该通过 CI/CD 自动执行,避免手动操作。
5.1 GitHub Actions 示例
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install
- name: Run database migrations
run: pnpm drizzle-kit migrate # 或 pnpx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}5.2 迁移最佳实践
- 迁移前备份:CI/CD 应该在执行迁移前备份数据库
- 原子执行:迁移应该在一个事务里执行,要么全部成功,要么全部回滚
- 失败告警:迁移失败时应该告警,阻止后续部署
- 幂等性:迁移应该可以重复执行而不出错
6. 大表 Schema 变更
大表(百万行以上)的 schema 变更可能锁表,导致服务不可用。需要特殊处理:
6.1 添加列
PostgreSQL 添加带默认值的列会锁表(9.1 版本之前):
-- ❌ 可能锁表
ALTER TABLE users ADD COLUMN role text DEFAULT 'user';
-- ✅ 分步执行
ALTER TABLE users ADD COLUMN role text;
ALTER TABLE users ALTER COLUMN role SET DEFAULT 'user';
UPDATE users SET role = 'user' WHERE role IS NULL;PostgreSQL 11+ 添加带默认值的列不会锁表。
6.2 删除列
删除列需要谨慎,因为应用代码可能还在引用:
1. 应用代码不再读取该列
2. 部署应用
3. 执行迁移删除列6.3 重命名列
重命名列需要应用代码同步更新:
1. 添加新列
2. 数据迁移(双写)
3. 应用代码切换到新列
4. 删除旧列6.4 在线迁移工具
对于超大规模数据库,可以使用在线迁移工具:
- pg_repack:无锁重建表
- gh-ost(GitHub):MySQL 在线 schema 变更
- Pt-online-schema-change(Percona):MySQL 在线变更
7. 数据迁移
Schema 迁移之外,有时还需要数据迁移(ETL、数据清洗等):
-- 数据迁移示例
UPDATE users
SET name = COALESCE(name, email)
WHERE name IS NULL;数据迁移应该:
- 分批执行:避免一次性更新大量数据
- 记录进度:便于中断后继续
- 可回滚:保留原始数据
// 分批更新示例
const batchSize = 1000
let offset = 0
while (true) {
const result = await db
.update(users)
.set({ name: sql`COALESCE(name, email)` })
.where(isNull(users.name))
.limit(batchSize)
if (result.length === 0) break
offset += batchSize
}8. 迁移冲突
多人协作时,可能同时生成迁移文件,导致冲突:
drizzle/
├── 0001_create_users.sql ← Alice 的迁移
├── 0002_add_email_index.sql ← Bob 的迁移
└── 0002_add_role_column.sql ← Charlie 的迁移(冲突)解决方法:
- 沟通协作:避免同时修改同一张表
- 重新生成:基于最新状态重新生成迁移
- 手动合并:合并冲突的迁移文件
9. 迁移测试
迁移应该在测试环境验证,避免生产环境出问题:
// tests/migration.test.ts
import { describe, it, expect } from 'vitest'
import { db } from '../src/lib/db'
import { users } from '../src/schema'
describe('migration', () => {
it('users table has expected columns', async () => {
const columns = await db.execute(sql`
SELECT column_name FROM information_schema.columns
WHERE table_name = 'users'
`)
expect(columns.map(c => c.column_name)).toContain('email')
expect(columns.map(c => c.column_name)).toContain('name')
})
})总结
迁移脚本管理是数据库变更的基础设施。Drizzle Kit 和 Prisma Migrate 提供了稳定的迁移工具。
这一节涉及到的几个实践:
- 迁移工具:Drizzle Kit 生成 SQL 文件,Prisma Migrate 交互式管理
- 版本控制:迁移文件纳入 git,便于协作和审计
- CI/CD 集成:自动应用迁移,迁移前备份,失败告警
- 回滚策略:Drizzle/Prisma 不直接支持回滚,需要手动编写或版本控制还原
- 大表变更:分批执行、避免锁表、在线迁移工具
- 数据迁移:分批更新、记录进度、可回滚
- 迁移冲突:沟通协作、重新生成、手动合并
- 迁移测试:在测试环境验证迁移
良好的迁移管理可以避免数据库变更带来的风险,保证多环境一致性。
下一篇看数据库性能优化——索引、查询优化、N+1 问题、慢查询分析。