迁移脚本管理

要点

  • 数据库迁移是把 schema 变更应用到数据库的过程,需要版本化、可重复、可回滚
  • Drizzle Kit 和 Prisma Migrate 是两种主流的迁移工具
  • 迁移文件应该纳入版本控制,便于协作和审计
  • 生产环境的迁移应该通过 CI/CD 自动执行,避免手动操作
  • 回滚策略需要提前规划,部分迁移不可逆(例如删除列、删除数据)
  • 大表的 schema 变更需要考虑锁表时间和在线迁移方案

1. 迁移的概念

数据库迁移(migration)是把 schema 变更(增删表、修改列、添加索引等)以脚本的形式管理,使得:

  1. 变更可重复执行(任何环境都能得到一致的数据库状态)
  2. 变更可追溯(通过迁移文件的历史可以看到数据库的演进)
  3. 变更可协作(团队成员可以共享迁移,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 status

2.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.toml

3.3 回滚

Prisma Migrate 同样不直接支持回滚。需要:

  1. 编写新的迁移来撤销变更
  2. 或者使用 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_init

4. 迁移与版本控制

迁移文件应该纳入版本控制(git),这样可以:

  1. 团队协作:其他成员拉取代码后可以执行迁移
  2. 审计:可以看到数据库的演进历史
  3. 环境一致性: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 迁移最佳实践

  1. 迁移前备份:CI/CD 应该在执行迁移前备份数据库
  2. 原子执行:迁移应该在一个事务里执行,要么全部成功,要么全部回滚
  3. 失败告警:迁移失败时应该告警,阻止后续部署
  4. 幂等性:迁移应该可以重复执行而不出错

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;

数据迁移应该:

  1. 分批执行:避免一次性更新大量数据
  2. 记录进度:便于中断后继续
  3. 可回滚:保留原始数据
// 分批更新示例
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 的迁移(冲突)

解决方法:

  1. 沟通协作:避免同时修改同一张表
  2. 重新生成:基于最新状态重新生成迁移
  3. 手动合并:合并冲突的迁移文件

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 提供了稳定的迁移工具。

这一节涉及到的几个实践:

  1. 迁移工具:Drizzle Kit 生成 SQL 文件,Prisma Migrate 交互式管理
  2. 版本控制:迁移文件纳入 git,便于协作和审计
  3. CI/CD 集成:自动应用迁移,迁移前备份,失败告警
  4. 回滚策略:Drizzle/Prisma 不直接支持回滚,需要手动编写或版本控制还原
  5. 大表变更:分批执行、避免锁表、在线迁移工具
  6. 数据迁移:分批更新、记录进度、可回滚
  7. 迁移冲突:沟通协作、重新生成、手动合并
  8. 迁移测试:在测试环境验证迁移

良好的迁移管理可以避免数据库变更带来的风险,保证多环境一致性。

下一篇看数据库性能优化——索引、查询优化、N+1 问题、慢查询分析。