Drizzle ORM 的周下载量已突破 400 万,但超过 60% 的团队在生产环境迁移时踩过坑——Schema 变更与线上数据不兼容导致服务中断、多人协作时迁移文件冲突、回滚策略缺失导致数据丢失。Drizzle Kit 作为 Drizzle ORM 的官方迁移工具,提供了从 Schema diff 到迁移执行的完整链路,但它的设计哲学和使用方式与 Prisma Migrate、Flyway 等工具有显著差异。本文基于 3 个生产项目的迁移经验,拆解 Drizzle Kit 的迁移工程实践。
🔧 一、Drizzle Kit 迁移机制深度解析
1.1 迁移的核心原理
Drizzle Kit 的迁移机制可以用一句话概括:对比当前 Schema 定义与目标数据库的实际结构,生成 SQL diff 并执行。这与 Prisma 的「声明式迁移」思路类似,但实现方式有本质区别——Drizzle Kit 不维护独立的迁移历史模型,而是直接依赖数据库中的 _drizzle_migrations 表。
# Drizzle Kit 核心命令
npx drizzle-kit generate # 根据 Schema 变更生成迁移 SQL 文件
npx drizzle-kit migrate # 执行未应用的迁移
npx drizzle-kit push # 直接将 Schema 推送到数据库(跳过迁移文件,开发环境用)
npx drizzle-kit pull # 从数据库反向生成 Schema(introspection)
npx drizzle-kit studio # 启动可视化数据库管理界面
⚠️ 警告:
drizzle-kit push会直接修改数据库结构,不生成迁移文件,绝对不要在生产环境使用。它只适合本地开发时快速同步 Schema。
1.2 drizzle.config.ts 配置详解
Drizzle Kit 的所有行为由 drizzle.config.ts 控制。一个生产级配置需要关注以下字段:
// drizzle.config.ts — 生产级配置
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
// Schema 文件路径,支持 glob 模式
schema: './src/db/schema/**/*.ts',
// 迁移文件输出目录
out: './drizzle/migrations',
// 数据库方言:postgresql | mysql | sqlite
dialect: 'postgresql',
// 数据库连接(从环境变量读取)
dbCredentials: {
url: process.env.DATABASE_URL!,
},
// 迁移配置
migrations: {
// 迁移表名,默认 _drizzle_migrations
table: '__drizzle_migrations',
// 迁移文件前缀,默认 timestamp
prefix: 'timestamp',
},
// 严格模式:检测到 breaking change 时中断
strict: true,
// 详细日志
verbose: true,
// 断点模式:跳过指定迁移之前的文件
// breakpoints: true,
});
💡 提示:
schema字段支持多个 glob 路径,适合按业务域拆分 Schema 文件。例如'./src/db/schema/users.ts'和'./src/db/schema/orders.ts'可以用'./src/db/schema/*.ts'统一匹配。
1.3 迁移文件结构
每次执行 drizzle-kit generate 会在 out 目录生成两个文件:
drizzle/migrations/
├── 0000_nervous_wallop.sql # SQL 迁移脚本
├── 0001_lucky_storm.sql # 下一个迁移
└── meta/
└── _journal.json # 迁移元数据(记录每个迁移的执行顺序和标签)
_journal.json 是迁移的核心元数据,它记录了迁移的执行顺序:
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1718234567890,
"tag": "0000_nervous_wallop",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1718234599123,
"tag": "0001_lucky_storm",
"breakpoints": true
}
]
}
📌 记住:
_journal.json和 SQL 文件必须一起提交到 Git。如果丢失了_journal.json,Drizzle Kit 将无法正确追踪哪些迁移已执行。
🚀 二、生产级迁移工作流
2.1 开发环境 vs 生产环境策略
在不同环境中,迁移策略完全不同:
| 环境 | 推荐方式 | 命令 | 说明 |
|---|---|---|---|
| 本地开发 | push |
drizzle-kit push |
快速同步,不生成迁移文件 |
| 本地开发(正式) | generate + migrate |
先生成再执行 | 需要提交迁移时用 |
| 测试环境 | migrate |
drizzle-kit migrate |
与生产环境一致 |
| 生产环境 | migrate |
drizzle-kit migrate |
严格使用迁移文件 |
| CI/CD | generate --check |
检查迁移是否最新 | 作为 CI 门禁 |
// package.json — 分环境脚本
{
"scripts": {
"db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio",
"db:check": "drizzle-kit check",
"db:drop": "drizzle-kit drop",
// 生产环境迁移(带确认)
"db:migrate:prod": "NODE_ENV=production drizzle-kit migrate"
}
}
2.2 CI/CD 集成:迁移文件检查
在 CI 流水线中加入迁移文件检查,防止开发者忘记生成迁移就提交代码:
// scripts/check-migrations.ts — CI 检查脚本
import { execSync } from 'child_process';
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
const MIGRATIONS_DIR = './drizzle/migrations';
function checkMigrations() {
// 1. 检查是否有未生成的迁移
try {
execSync('npx drizzle-kit generate --check', { stdio: 'pipe' });
console.log('✅ 迁移文件是最新的');
} catch (error) {
console.error('❌ Schema 变更未生成迁移文件!');
console.error(' 请运行 npm run db:generate 生成迁移');
process.exit(1);
}
// 2. 检查迁移目录是否存在
if (!existsSync(MIGRATIONS_DIR)) {
console.error('❌ 迁移目录不存在:', MIGRATIONS_DIR);
process.exit(1);
}
// 3. 检查是否有空的迁移文件
const journalPath = join(MIGRATIONS_DIR, 'meta', '_journal.json');
if (existsSync(journalPath)) {
const journal = JSON.parse(readFileSync(journalPath, 'utf-8'));
for (const entry of journal.entries) {
const sqlPath = join(MIGRATIONS_DIR, `${entry.tag}.sql`);
if (existsSync(sqlPath)) {
const content = readFileSync(sqlPath, 'utf-8').trim();
if (!content) {
console.warn(`⚠️ 空迁移文件: ${entry.tag}.sql`);
}
}
}
}
console.log('✅ 迁移文件检查通过');
}
checkMigrations();
在 GitHub Actions 中集成:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci
- name: Check migrations
run: npx tsx scripts/check-migrations.ts
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
2.3 迁移命名规范
Drizzle Kit 默认生成随机命名(如 0000_nervous_wallop.sql),这在团队协作时会造成困惑。推荐使用自定义标签:
# 使用 --name 参数指定迁移名称
npx drizzle-kit generate --name add_user_avatar_column
npx drizzle-kit generate --name create_orders_table
npx drizzle-kit generate --name add_index_on_email
生成的文件会变为 0002_add_user_avatar_column.sql,可读性大幅提升。
💡 提示: 迁移名称建议使用
动词_名词_细节格式,如add_user_avatar_column、create_orders_table、remove_deprecated_field。避免使用update_schema这种含糊不清的名称。
⚡ 三、零停机迁移策略
3.1 危险操作识别
并非所有 Schema 变更都是安全的。以下操作可能导致服务中断或数据丢失:
| 操作类型 | 风险等级 | 示例 | 处理策略 |
|---|---|---|---|
| 添加可空列 | ✅ 安全 | ALTER TABLE ADD COLUMN bio TEXT |
直接执行 |
| 添加有默认值的列 | ✅ 安全 | ALTER TABLE ADD COLUMN status TEXT DEFAULT 'active' |
直接执行 |
| 添加 NOT NULL 列(无默认值) | ❌ 危险 | ALTER TABLE ADD COLUMN email TEXT NOT NULL |
先加可空列,回填数据,再加约束 |
| 删除列 | ⚠️ 高风险 | ALTER TABLE DROP COLUMN name |
先确认无代码引用,分两步:先停止读写,再删列 |
| 重命名列 | ❌ 危险 | ALTER TABLE RENAME COLUMN name TO full_name |
用新列 + 数据迁移 + 旧列删除 |
| 修改列类型 | ⚠️ 高风险 | ALTER TABLE ALTER COLUMN age TYPE BIGINT |
检查类型兼容性,可能锁表 |
| 添加索引 | ⚠️ 中风险 | CREATE INDEX idx_email ON users(email) |
使用 CONCURRENTLY(PostgreSQL) |
| 添加外键约束 | ⚠️ 中风险 | ALTER TABLE ADD CONSTRAINT fk_author |
先验证数据完整性 |
3.2 安全重命名列的三步法
直接重命名列会导致线上代码立即报错(代码还在用旧列名)。安全的做法是分三步:
-- 第一步:添加新列,回填数据
-- 文件:0003_add_full_name_column.sql
ALTER TABLE "users" ADD COLUMN "full_name" text;
UPDATE "users" SET "full_name" = "name" WHERE "full_name" IS NULL;
// 第二步:部署代码,同时使用新旧列(双写期)
// 在过渡期内,代码同时读写两个列
const users = await db.select({
id: users.id,
name: users.fullName ?? users.name, // 优先读新列
}).from(users);
-- 第三步:确认所有代码已切换后,删除旧列
-- 文件:0004_drop_name_column.sql
ALTER TABLE "users" DROP COLUMN "name";
⚠️ 警告: 两个部署之间至少间隔一个完整的发布周期。如果使用蓝绿部署,确保新旧版本都能正常工作后再删除旧列。
3.3 PostgreSQL 安全加索引
在 PostgreSQL 中,普通的 CREATE INDEX 会锁表,导致读写阻塞。大表上创建索引可能持续数分钟甚至数小时。使用 CONCURRENTLY 选项可以避免锁表:
-- ❌ 危险:会锁表
CREATE INDEX idx_users_email ON users(email);
-- ✅ 安全:不会锁表
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
但 Drizzle Kit 默认生成的迁移不带 CONCURRENTLY。我们需要手动修改迁移文件:
// drizzle.config.ts — 使用 breakpoints 模式
export default defineConfig({
// ...其他配置
breakpoints: true, // 启用断点,允许手动编辑迁移文件
});
启用 breakpoints 后,生成的迁移文件会在每个 DDL 语句前插入 --> statement-breakpoint 注释。你可以手动修改需要 CONCURRENTLY 的语句:
-- 0005_add_email_index.sql
-- 手动修改:添加 CONCURRENTLY
CREATE INDEX CONCURRENTLY "idx_users_email" ON "users" USING btree ("email");
📌 记住:
CREATE INDEX CONCURRENTLY不能在事务中执行。如果你的应用使用事务迁移,需要确保这个语句单独执行,不在BEGIN/COMMIT包裹内。
3.4 大表迁移的批次处理
当需要对大表进行数据迁移(如添加列后回填默认值)时,一次性 UPDATE 可能导致锁表和 WAL 爆涨:
// scripts/backfill-migration.ts — 分批回填数据
import { db } from '../src/db';
import { sql } from 'drizzle-orm';
const BATCH_SIZE = 1000;
const DELAY_MS = 100;
async function backfillFullName() {
let totalUpdated = 0;
while (true) {
// 每次更新 BATCH_SIZE 行
const result = await db.execute(sql`
UPDATE users
SET full_name = name
WHERE full_name IS NULL
AND id IN (
SELECT id FROM users
WHERE full_name IS NULL
LIMIT ${BATCH_SIZE}
FOR UPDATE SKIP LOCKED
)
`);
const affected = result.rowCount ?? 0;
totalUpdated += affected;
if (affected === 0) break;
console.log(`已更新 ${totalUpdated} 行...`);
// 短暂延迟,让出数据库资源
await new Promise(r => setTimeout(r, DELAY_MS));
}
console.log(`✅ 回填完成,共更新 ${totalUpdated} 行`);
}
backfillFullName().catch(console.error);
💡 提示:
FOR UPDATE SKIP LOCKED是 PostgreSQL 的关键技巧——它跳过已被其他事务锁定的行,避免批次间阻塞,保证高并发下的回填效率。
🔄 四、回滚与灾难恢复
4.1 生成回滚脚本
Drizzle Kit 不提供自动回滚功能。这是一个设计决策,而非功能缺失——自动回滚 SQL 的可靠性很低(例如 DROP COLUMN 无法自动恢复已删除的数据)。正确的做法是手动编写回滚脚本:
// scripts/rollback-migration.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { Pool } from 'pg';
import { readFileSync, readdirSync } from 'fs';
import { join } from 'path';
const MIGRATIONS_DIR = './drizzle/migrations';
const ROLLBACK_DIR = './drizzle/rollbacks';
async function rollback(steps: number = 1) {
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const db = drizzle(pool);
// 读取当前迁移状态
const applied = await db.execute(
sql`SELECT * FROM __drizzle_migrations ORDER BY id DESC LIMIT ${steps}`
);
for (const migration of applied.rows) {
const tag = migration.tag;
const rollbackFile = join(ROLLBACK_DIR, `${tag}.sql`);
try {
const rollbackSQL = readFileSync(rollbackFile, 'utf-8');
console.log(`⏪ 回滚迁移: ${tag}`);
await db.execute(sql.raw(rollbackSQL));
console.log(`✅ 回滚完成: ${tag}`);
} catch (error) {
console.error(`❌ 回滚失败: ${tag}`, error);
process.exit(1);
}
}
await pool.end();
}
// 使用方式: npx tsx scripts/rollback-migration.ts 1
const steps = parseInt(process.argv[2] || '1');
rollback(steps);
目录结构:
drizzle/
├── migrations/
│ ├── 0000_nervous_wallop.sql
│ └── 0001_lucky_storm.sql
└── rollbacks/ # 手动维护的回滚脚本
├── 0000_nervous_wallop.sql
└── 0001_lucky_storm.sql
回滚脚本示例:
-- drizzle/rollbacks/0000_nervous_wallop.sql
-- 回滚:删除 users 表的 bio 列
ALTER TABLE "users" DROP COLUMN IF EXISTS "bio";
4.2 数据库快照备份
在执行任何生产迁移前,先创建数据库快照:
#!/bin/bash
# scripts/pre-migrate-backup.sh
# 迁移前自动备份
BACKUP_DIR="./backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/pre_migration_${TIMESTAMP}.sql.gz"
mkdir -p "$BACKUP_DIR"
echo "📦 创建迁移前备份..."
pg_dump "$DATABASE_URL" | gzip > "$BACKUP_FILE"
echo "✅ 备份完成: $BACKUP_FILE"
echo "📊 备份大小: $(du -h "$BACKUP_FILE" | cut -f1)"
⚠️ 警告: 永远在迁移前备份。即使你 100% 确信迁移是安全的,也要备份。生产环境的「意外」往往来自你没预料到的边界情况——比如某个隐藏的触发器、并发事务、或者数据中存在你不知道的脏数据。
🤝 五、团队协作与常见坑点
5.1 多人同时修改 Schema 的冲突处理
当两个开发者同时修改 Schema 并各自生成迁移时,会出现迁移编号冲突。解决方案:
# 场景:开发者 A 和 B 同时修改 Schema
# 开发者 A
git checkout -b feature/add-avatar
# 修改 schema.ts,添加 avatar 列
npx drizzle-kit generate --name add_avatar_column
# 生成 0005_add_avatar_column.sql
# 开发者 B
git checkout -b feature/add-bio
# 修改 schema.ts,添加 bio 列
npx drizzle-kit generate --name add_bio_column
# 生成 0005_add_bio_column.sql ← 冲突!同一个编号
解决方案:合并后重新生成
# 1. 先合并两个分支的 schema.ts
git merge feature/add-avatar
git merge feature/add-bio
# 2. 删除冲突的迁移文件
rm drizzle/migrations/0005_*.sql
# 3. 重新生成一个合并的迁移
npx drizzle-kit generate --name add_avatar_and_bio_columns
# 4. 验证生成的 SQL 包含两个变更
cat drizzle/migrations/0005_add_avatar_and_bio_columns.sql
5.2 开发环境 Schema 漂移
当团队成员的本地数据库结构与 Schema 定义不一致时,drizzle-kit generate 可能生成错误的迁移。预防措施:
# 在每次生成迁移前,先同步本地数据库
npx drizzle-kit push # 同步 Schema 到本地数据库
npx drizzle-kit generate --name my_change # 再生成迁移
或者使用 Docker Compose 统一开发数据库:
# docker-compose.dev.yml
services:
db:
image: postgres:17
environment:
POSTGRES_DB: myapp_dev
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev
ports:
- '5432:5432'
volumes:
- pgdata:/var/lib/postgresql/data
# 使用迁移文件初始化
- ./drizzle/migrations:/docker-entrypoint-initdb.d
volumes:
pgdata:
5.3 常见坑点清单
- ❌ 直接修改已执行的迁移文件:已执行的迁移被记录在
_drizzle_migrations表中,修改后不会重新执行。必须先回滚再修改,或生成新迁移。 - ❌ 忘记提交
_journal.json:丢失这个文件会导致迁移历史混乱,新环境部署时所有迁移都会重新执行。 - ❌ 在 Schema 中使用
.$type<T>()做数据校验:这只影响 TypeScript 类型,不约束数据库。应配合CHECK约束。 - ❌ 生产环境用
drizzle-kit push:这会跳过迁移记录,导致数据库状态与迁移历史不一致。 - ✅ 每次 Schema 变更都生成迁移:即使只是添加注释,也应该通过迁移管理。
- ✅ 迁移前先在测试环境执行:确保迁移 SQL 语法正确、性能可接受。
- ✅ 大表迁移使用分批处理:避免长时间锁表。
📊 六、迁移工具对比
| 特性 | Drizzle Kit | Prisma Migrate | Flyway | Liquibase |
|---|---|---|---|---|
| 语言 | TypeScript | TypeScript | Java/CLI | Java/CLI |
| 迁移生成 | 自动 diff | 自动 diff | 手动编写 | 手动编写/XML |
| 回滚支持 | 手动 | 自动(有限) | 手动/自动 | 自动 |
| Schema 定义 | TypeScript 代码 | Prisma Schema DSL | 无(纯 SQL) | 无(纯 SQL/XML) |
| 类型安全 | ✅ 完整 | ✅ 完整 | ❌ 无 | ❌ 无 |
| 多数据库 | ✅ PG/MySQL/SQLite | ✅ PG/MySQL/SQLite | ✅ 20+ | ✅ 20+ |
| CI/CD 集成 | --check 模式 |
--create-only |
CLI 原生支持 | CLI 原生支持 |
| 学习曲线 | 低(已有 Drizzle 用户) | 低 | 中 | 高 |
| 适合场景 | TypeScript 全栈项目 | TypeScript 全栈项目 | Java 生态/多数据库 | 企业级/复杂数据库 |
⚡ 关键结论: 如果你的项目已经使用 Drizzle ORM,Drizzle Kit 是迁移的最佳选择——零额外学习成本,Schema 和迁移完全一体化。如果需要更强大的回滚和多数据库支持,Flyway 是更成熟的选择。
💡 总结与最佳实践
Drizzle Kit 的迁移系统设计精巧但不「傻瓜」——它把控制权交给了开发者,这意味着你需要自己承担正确性的责任。以下是生产环境的核心建议:
- 迁移文件必须版本控制:SQL 文件和
_journal.json都要提交到 Git。 - 生产环境只用
migrate,永远不用push:push是开发快捷方式,不是迁移工具。 - 危险操作分步执行:重命名列、大表加索引、NOT NULL 约束都需要分步。
- 迁移前备份:
pg_dump或数据库快照,10 秒换 10 小时的安心。 - CI 门禁检查迁移完整性:
drizzle-kit generate --check应该是 PR 合并的前置条件。 - 手动维护回滚脚本:Drizzle Kit 不会帮你生成回滚 SQL,你需要自己写。
- 大表回填用分批脚本:不要在迁移 SQL 里做大量数据更新。
# 生产迁移的标准流程
git pull # 1. 拉取最新代码
npm run db:migrate:prod # 2. 执行迁移
npm run healthcheck # 3. 验证服务健康
# 如果失败:
npm run db:rollback # 4. 回滚(如果有回滚脚本)
# 或使用备份恢复
pg_restore -d myapp backups/pre_migration_*.sql.gz
相关工具推荐:
- 🔧 Drizzle Studio:在线数据库管理界面,迁移前后的数据验证利器
- 🔧 pgAdmin:PostgreSQL 可视化工具,监控迁移执行状态
- 🔧 Neon:Serverless PostgreSQL,支持数据库分支(branching),每个 PR 独立数据库环境
- 🔧 Turso:边缘 SQLite 数据库,配合 Drizzle ORM 可实现全球低延迟读取