29c - 迁移文件生成与审查
本文是《AI Agent 实战手册》第 29 章第 3 节。 上一节:29b-AI辅助Schema设计 | 下一节:29d-ORM与查询优化
概述
数据库迁移(Migration)是 Schema 变更从开发到生产的关键桥梁——一次不安全的迁移可能导致数据丢失、服务中断甚至不可逆的灾难。2025-2026 年,AI 编码助手(Claude Code、Cursor、Kiro)能够自动生成迁移文件,但”生成容易、审查难”:AI 生成的迁移可能包含破坏性操作(DROP COLUMN)、锁表风险(ALTER TABLE 大表)或缺少回滚策略。本节系统覆盖六大主流 ORM/迁移工具(Prisma、Drizzle、SQLAlchemy+Alembic、TypeORM、Diesel、Django)的迁移文件生成流程,深入讲解 AI 辅助迁移安全审查、零停机迁移模式、回滚策略、CI/CD 集成和 Kiro Hooks 自动化审查,提供完整的提示词模板和实战案例。
1. 迁移工具全景与对比
1.1 主流迁移工具对比
| 工具 | 语言/框架 | 迁移方式 | AI 友好度 | 回滚支持 | 价格 | 适用场景 |
|---|---|---|---|---|---|---|
| Prisma Migrate | TypeScript/JavaScript | Schema-first 声明式 | ⭐⭐⭐⭐⭐ AI Safety Guardrails | ✅ migrate diff + 手动 down | 免费(开源)/ Prisma Accelerate $0.20/100K ops | Node.js 全栈项目 |
| Drizzle Kit | TypeScript/JavaScript | Schema-first 声明式 | ⭐⭐⭐⭐ SQL 可读性强 | ✅ 自动生成 SQL,手动回滚 | 免费(开源) | 轻量 TypeScript 项目 |
| Alembic | Python (SQLAlchemy) | 命令式 + autogenerate | ⭐⭐⭐⭐ 灵活可定制 | ✅ downgrade() 函数 | 免费(开源) | Python/FastAPI/Flask 项目 |
| TypeORM | TypeScript/JavaScript | Entity-first 自动生成 | ⭐⭐⭐ 需要 DataSource 配置 | ✅ down() 方法 | 免费(开源) | NestJS/Express 项目 |
| Diesel | Rust | SQL-first 手写 | ⭐⭐⭐ 编译时安全 | ✅ down.sql 文件 | 免费(开源) | Rust 后端项目 |
| Django Migrations | Python (Django) | Model-first 自动生成 | ⭐⭐⭐⭐ 成熟稳定 | ✅ migrate app 000X 回滚 | 免费(开源) | Django 项目 |
| Atlas | Go(多语言支持) | 声明式 + 版本化 | ⭐⭐⭐⭐⭐ CI/CD 原生集成 | ✅ 声明式回滚 | 免费(开源)/ Atlas Cloud $0 起 | 多数据库企业项目 |
| Flyway | Java(多语言支持) | SQL-first 版本化 | ⭐⭐⭐ 传统但稳定 | ✅ Undo migrations(Teams 版) | 免费(Community)/ $497/年(Teams) | Java/企业级项目 |
| Liquibase | Java(多语言支持) | XML/YAML/SQL 变更集 | ⭐⭐⭐ 格式多样 | ✅ rollback 标签 | 免费(开源)/ $175/月(Pro) | 多数据库企业项目 |
| Bytebase | Go(Web UI) | SQL 审查 + GitOps | ⭐⭐⭐⭐⭐ AI SQL Review | ✅ 审批工作流 | 免费(Community)/ $79/月(Pro) | 团队协作数据库变更管理 |
1.2 迁移方式分类
┌─────────────────────────────────────────────────────────────┐
│ 数据库迁移方式分类 │
├─────────────────────┬───────────────────────────────────────┤
│ 声明式(Declarative)│ 命令式(Imperative) │
│ │ │
│ 定义"目标状态" │ 定义"变更步骤" │
│ 工具自动计算 diff │ 开发者手写 up/down │
│ │ │
│ ✅ Prisma │ ✅ Alembic(手动模式) │
│ ✅ Drizzle │ ✅ TypeORM │
│ ✅ Atlas │ ✅ Diesel │
│ ✅ Django │ ✅ Flyway │
│ │ ✅ Liquibase │
├─────────────────────┴───────────────────────────────────────┤
│ 混合模式:Alembic autogenerate(声明式生成 + 命令式微调) │
└─────────────────────────────────────────────────────────────┘2. Prisma Migrate:声明式迁移与 AI 安全护栏
2.1 Prisma 迁移核心命令
Prisma 采用 Schema-first 声明式迁移:开发者修改 schema.prisma 文件,Prisma 自动计算 diff 并生成 SQL 迁移文件。
| 命令 | 用途 | 环境 | 安全级别 |
|---|---|---|---|
prisma migrate dev | 开发环境迁移:生成 SQL + 应用 + 重新生成 Client | 开发 | ⚠️ 可能重置数据 |
prisma migrate deploy | 生产环境迁移:仅应用已有迁移文件 | 生产/CI | ✅ 安全,不生成新迁移 |
prisma db push | 快速原型:直接推送 Schema 到数据库,不生成迁移文件 | 原型/开发 | ❌ 无迁移历史 |
prisma migrate diff | 比较两个 Schema 状态的差异,生成 SQL | 审查/CI | ✅ 只读操作 |
prisma migrate reset | 重置数据库:删除所有数据,重新应用所有迁移 | 开发 | ❌ 破坏性操作 |
prisma migrate resolve | 标记迁移为已应用或已回滚(修复迁移状态) | 生产 | ⚠️ 需谨慎使用 |
开发环境迁移流程
# 1. 修改 schema.prisma(添加新字段)
# 2. 生成并应用迁移
npx prisma migrate dev --name add_user_avatar
# 生成的文件结构:
# prisma/migrations/
# ├── 20250615120000_add_user_avatar/
# │ └── migration.sql
# └── migration_lock.toml生成的迁移 SQL 示例
-- prisma/migrations/20250615120000_add_user_avatar/migration.sql
-- CreateTable 或 AlterTable
ALTER TABLE "User" ADD COLUMN "avatar_url" TEXT;
ALTER TABLE "User" ADD COLUMN "avatar_updated_at" TIMESTAMP(3);
-- CreateIndex
CREATE INDEX "User_avatar_updated_at_idx" ON "User"("avatar_updated_at");生产环境部署
# 在 CI/CD 中使用 migrate deploy(不会生成新迁移)
npx prisma migrate deploy
# 检查迁移状态
npx prisma migrate status2.2 Prisma AI Safety Guardrails(v6.15+)
Prisma ORM v6.15 引入了业界首个 ORM 级别的 AI 安全护栏,专门防止 AI 工具自动执行破坏性数据库操作:
┌─────────────────────────────────────────────────────────┐
│ Prisma AI Safety Guardrails │
├─────────────────────────────────────────────────────────┤
│ │
│ AI Agent 请求执行: │
│ prisma migrate reset │
│ prisma db push --force-reset │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 🛡️ Safety Guardrail 拦截 │ │
│ │ │ │
│ │ 检测到破坏性操作! │ │
│ │ 此操作将删除所有数据并重建数据库。 │ │
│ │ │ │
│ │ ❌ 自动执行被阻止 │ │
│ │ ✅ 需要人工确认才能继续 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 被保护的命令: │
│ • prisma migrate reset │
│ • prisma db push --force-reset │
│ • prisma db push --accept-data-loss │
│ • prisma migrate dev(当需要重置时) │
│ │
└─────────────────────────────────────────────────────────┘配置 AI Safety Guardrails
// schema.prisma
generator client {
provider = "prisma-client-js"
// AI Safety Guardrails 在 v6.15+ 默认启用
// 无需额外配置
}在 Kiro/Claude Code 中的行为
当 AI 编码助手尝试执行破坏性 Prisma 命令时:
# AI 尝试执行:
$ npx prisma migrate reset
# Prisma 检测到非交互式环境(AI 工具),自动阻止:
Error: This command will reset your database and delete all data.
This operation was blocked because it was detected as being run
by an AI tool. Please run this command manually to confirm.2.3 Prisma 迁移安全审查提示词
提示词模板:审查 Prisma 迁移文件
你是一位资深数据库工程师。请审查以下 Prisma 生成的迁移 SQL 文件,
检查安全风险并提供改进建议。
## 迁移文件内容
[粘贴 migration.sql 内容]
## 当前数据库规模
- 表 [表名] 约 [行数] 行
- 数据库类型:[PostgreSQL/MySQL/SQLite]
- 是否有读副本:[是/否]
## 审查清单
请逐项检查:
1. **破坏性操作**:是否包含 DROP TABLE、DROP COLUMN、TRUNCATE?
2. **数据丢失风险**:ALTER COLUMN 是否可能导致数据截断?
3. **锁表风险**:ALTER TABLE 是否会在大表上获取排他锁?
4. **索引创建**:CREATE INDEX 是否使用了 CONCURRENTLY(PostgreSQL)?
5. **默认值**:新增 NOT NULL 列是否提供了默认值?
6. **回滚可行性**:此迁移是否可以安全回滚?如何回滚?
7. **向后兼容性**:旧版本代码是否能与新 Schema 兼容?
## 输出格式
- 🔴 高风险:必须修复
- 🟡 中风险:建议修复
- 🟢 低风险:可接受
- 对每个风险项提供具体的修复建议提示词模板:生成安全的 Prisma Schema 变更
你是一位 Prisma 专家。请帮我安全地修改 Schema 以实现以下需求:
## 当前 Schema
[粘贴当前 schema.prisma 相关部分]
## 需求变更
[描述需要的变更]
## 安全要求
1. 不能有数据丢失风险
2. 必须支持零停机部署(旧代码和新代码同时运行)
3. 如果需要多步迁移,请拆分为多个步骤并说明执行顺序
4. 对于大表(>100万行),避免锁表操作
## 输出
1. 修改后的 schema.prisma 片段
2. 预期生成的 SQL(用于审查)
3. 部署步骤和注意事项
4. 回滚方案3. Drizzle Kit:SQL 可读的声明式迁移
3.1 Drizzle 迁移核心命令
Drizzle 采用 TypeScript Schema 定义 + SQL 迁移文件的方式,生成的 SQL 可读性极强,非常适合 AI 审查:
| 命令 | 用途 | 环境 | 安全级别 |
|---|---|---|---|
drizzle-kit generate | 根据 Schema 变更生成 SQL 迁移文件 | 开发 | ✅ 只生成文件,不执行 |
drizzle-kit migrate | 应用迁移文件到数据库 | 开发/生产 | ⚠️ 执行 SQL |
drizzle-kit push | 直接推送 Schema 到数据库(无迁移文件) | 原型/开发 | ❌ 无迁移历史 |
drizzle-kit pull | 从数据库反向生成 TypeScript Schema | 逆向工程 | ✅ 只读操作 |
drizzle-kit check | 检查迁移文件一致性 | CI | ✅ 只读操作 |
drizzle-kit studio | 启动可视化数据库浏览器 | 开发 | ✅ 只读浏览 |
Drizzle Schema 定义示例
// src/db/schema.ts
import { pgTable, uuid, varchar, timestamp, text, index, boolean } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: varchar('email', { length: 255 }).notNull().unique(),
name: varchar('name', { length: 100 }).notNull(),
avatarUrl: text('avatar_url'),
isActive: boolean('is_active').notNull().default(true),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
}, (table) => [
index('users_email_idx').on(table.email),
index('users_created_at_idx').on(table.createdAt),
]);
export const posts = pgTable('posts', {
id: uuid('id').primaryKey().defaultRandom(),
title: varchar('title', { length: 255 }).notNull(),
content: text('content'),
authorId: uuid('author_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
publishedAt: timestamp('published_at'),
createdAt: timestamp('created_at').notNull().defaultNow(),
}, (table) => [
index('posts_author_id_idx').on(table.authorId),
index('posts_published_at_idx').on(table.publishedAt),
]);生成迁移文件
# drizzle.config.ts 配置
# export default defineConfig({
# schema: './src/db/schema.ts',
# out: './drizzle',
# dialect: 'postgresql',
# });
# 生成迁移
npx drizzle-kit generate
# 生成的文件结构:
# drizzle/
# ├── 0000_initial_schema.sql
# ├── 0001_add_posts_table.sql
# └── meta/
# ├── 0000_snapshot.json
# ├── 0001_snapshot.json
# └── _journal.json生成的 SQL 示例
-- drizzle/0001_add_posts_table.sql
CREATE TABLE IF NOT EXISTS "posts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"title" varchar(255) NOT NULL,
"content" text,
"author_id" uuid NOT NULL,
"published_at" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL
);
-- 外键约束
ALTER TABLE "posts"
ADD CONSTRAINT "posts_author_id_users_id_fk"
FOREIGN KEY ("author_id") REFERENCES "users"("id")
ON DELETE cascade ON UPDATE no action;
-- 索引
CREATE INDEX IF NOT EXISTS "posts_author_id_idx" ON "posts" USING btree ("author_id");
CREATE INDEX IF NOT EXISTS "posts_published_at_idx" ON "posts" USING btree ("published_at");3.2 Drizzle 迁移的 AI 审查要点
Drizzle 生成的 SQL 非常干净,但仍需注意以下问题:
// ❌ 危险:直接删除列(Drizzle 会生成 ALTER TABLE DROP COLUMN)
// 修改前的 Schema
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: varchar('email', { length: 255 }).notNull(),
oldField: text('old_field'), // 删除这个字段
});
// 修改后的 Schema(直接删除 oldField)
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: varchar('email', { length: 255 }).notNull(),
// oldField 被删除
});
// Drizzle 会生成:
// ALTER TABLE "users" DROP COLUMN "old_field";
// ⚠️ 这是不可逆操作!数据将永久丢失!// ✅ 安全:分步迁移
// 步骤 1:标记字段为废弃(添加注释,但不删除)
// 步骤 2:确认所有代码不再读取该字段
// 步骤 3:在下一个版本中删除字段
// 步骤 4:在确认回滚窗口过后,清理迁移3.3 Drizzle 迁移提示词模板
你是一位 Drizzle ORM 专家。请审查以下 Drizzle 生成的迁移 SQL。
## 迁移 SQL
[粘贴 drizzle 生成的 .sql 文件内容]
## Schema 变更描述
[描述从什么变到什么]
## 审查要点
1. 是否有 DROP COLUMN/DROP TABLE 操作?如果有,是否可以改为分步迁移?
2. ALTER TABLE 操作是否会在大表上造成锁表?
3. 新增的 NOT NULL 列是否有 DEFAULT 值?
4. 外键约束是否正确设置了 ON DELETE 行为?
5. 索引创建是否合理?是否需要 CONCURRENTLY?
6. 此迁移是否支持回滚?请提供回滚 SQL。
## 输出
- 风险评估(🔴/🟡/🟢)
- 具体问题和修复建议
- 回滚 SQL
- 部署注意事项4. SQLAlchemy + Alembic:Python 生态的迁移标准
4.1 Alembic 迁移核心命令
Alembic 是 SQLAlchemy 的官方迁移工具,支持 autogenerate(自动检测 Model 变更)和手动编写迁移脚本:
| 命令 | 用途 | 环境 | 安全级别 |
|---|---|---|---|
alembic revision --autogenerate -m "msg" | 自动检测 Model 变更,生成迁移脚本 | 开发 | ⚠️ 需人工审查 |
alembic revision -m "msg" | 创建空白迁移脚本(手动编写) | 开发 | ✅ 完全可控 |
alembic upgrade head | 应用所有未执行的迁移 | 开发/生产 | ⚠️ 执行 SQL |
alembic upgrade +1 | 只应用下一个迁移 | 生产 | ✅ 逐步执行 |
alembic downgrade -1 | 回滚最近一次迁移 | 生产 | ⚠️ 需确认回滚安全 |
alembic downgrade base | 回滚所有迁移 | 开发 | ❌ 破坏性操作 |
alembic current | 查看当前迁移版本 | 任何 | ✅ 只读操作 |
alembic history | 查看迁移历史 | 任何 | ✅ 只读操作 |
alembic check | 检查是否有未生成的 Model 变更 | CI | ✅ 只读操作 |
Alembic 项目结构
project/
├── alembic/
│ ├── env.py # 迁移环境配置
│ ├── script.py.mako # 迁移脚本模板
│ └── versions/ # 迁移文件目录
│ ├── 001_abc123_initial.py
│ ├── 002_def456_add_posts.py
│ └── 003_ghi789_add_avatar.py
├── alembic.ini # Alembic 配置文件
├── app/
│ └── models.py # SQLAlchemy Models
└── requirements.txtautogenerate 迁移示例
# app/models.py - SQLAlchemy Model
from sqlalchemy import Column, String, Text, Boolean, DateTime, ForeignKey, Index
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import DeclarativeBase, relationship
from datetime import datetime
import uuid
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = 'users'
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
email = Column(String(255), nullable=False, unique=True)
name = Column(String(100), nullable=False)
avatar_url = Column(Text, nullable=True) # 新增字段
is_active = Column(Boolean, nullable=False, default=True)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
posts = relationship('Post', back_populates='author')
__table_args__ = (
Index('ix_users_email', 'email'),
Index('ix_users_created_at', 'created_at'),
)# 生成迁移
alembic revision --autogenerate -m "add user avatar"
# 生成的迁移文件:alembic/versions/003_ghi789_add_avatar.py生成的迁移脚本
"""add user avatar
Revision ID: ghi789
Revises: def456
Create Date: 2025-06-15 12:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers
revision: str = 'ghi789'
down_revision: Union[str, None] = 'def456'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic ###
op.add_column('users', sa.Column('avatar_url', sa.Text(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic ###
op.drop_column('users', 'avatar_url')
# ### end Alembic commands ###4.2 Alembic autogenerate 的局限性
Alembic autogenerate 无法检测所有变更,以下情况需要手动编写:
| 可自动检测 | 不可自动检测(需手动) |
|---|---|
| 新增/删除表 | 表名重命名 |
| 新增/删除列 | 列名重命名 |
| 列类型变更 | 约束名变更 |
| 新增/删除索引 | 数据迁移(Data Migration) |
| 外键变更 | 存储过程/触发器 |
| 唯一约束变更 | 自定义类型(Enum 值变更) |
| 默认值变更 | 分区表操作 |
手动数据迁移示例
"""migrate user status from string to enum
Revision ID: jkl012
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import ENUM
revision = 'jkl012'
down_revision = 'ghi789'
# 定义新的 Enum 类型
user_status = ENUM('active', 'inactive', 'suspended', name='user_status', create_type=False)
def upgrade() -> None:
# 步骤 1:创建 Enum 类型
user_status.create(op.get_bind(), checkfirst=True)
# 步骤 2:添加新列
op.add_column('users', sa.Column('status', user_status, nullable=True))
# 步骤 3:数据迁移(将旧数据转换到新列)
op.execute("""
UPDATE users
SET status = CASE
WHEN is_active = true THEN 'active'::user_status
ELSE 'inactive'::user_status
END
""")
# 步骤 4:设置 NOT NULL
op.alter_column('users', 'status', nullable=False)
# 步骤 5:删除旧列(在确认安全后的下一次迁移中执行)
# op.drop_column('users', 'is_active') # ⚠️ 不要在同一迁移中删除!
def downgrade() -> None:
op.drop_column('users', 'status')
user_status.drop(op.get_bind(), checkfirst=True)4.3 Alembic 迁移提示词模板
你是一位 SQLAlchemy/Alembic 专家。请帮我编写安全的 Alembic 迁移脚本。
## 当前 Model
[粘贴当前 SQLAlchemy Model 代码]
## 需求变更
[描述需要的变更]
## 要求
1. 使用 Alembic op 操作(不要直接写 SQL,除非必要)
2. upgrade() 和 downgrade() 必须完整且可逆
3. 如果涉及数据迁移,使用 op.execute() 并确保幂等性
4. 对于大表操作,考虑分批处理
5. 新增 NOT NULL 列必须先添加为 nullable=True,填充数据后再改为 NOT NULL
6. 如果 autogenerate 无法检测(如列重命名),请手动编写
## 输出
1. 完整的迁移脚本(upgrade + downgrade)
2. 执行前的安全检查清单
3. 预估执行时间和锁表影响
4. 回滚验证步骤5. TypeORM:Entity-First 自动迁移
5.1 TypeORM 迁移核心命令
TypeORM 采用 Entity-first 方式:开发者定义 TypeScript Entity,TypeORM 自动比较 Entity 与数据库的差异并生成迁移文件。
| 命令 | 用途 | 环境 | 安全级别 |
|---|---|---|---|
typeorm migration:generate -n Name | 比较 Entity 与数据库,自动生成迁移 | 开发 | ⚠️ 需要数据库连接 |
typeorm migration:create -n Name | 创建空白迁移文件(手动编写) | 开发 | ✅ 完全可控 |
typeorm migration:run | 执行所有未运行的迁移 | 开发/生产 | ⚠️ 执行 SQL |
typeorm migration:revert | 回滚最近一次迁移 | 生产 | ⚠️ 需确认回滚安全 |
typeorm migration:show | 显示所有迁移及其状态 | 任何 | ✅ 只读操作 |
typeorm schema:sync | 直接同步 Entity 到数据库(无迁移文件) | 开发 | ❌ 生产禁用 |
⚠️ 重要:生产环境必须设置
synchronize: false,只使用migration:run。
TypeORM DataSource 配置
// src/data-source.ts
import { DataSource } from 'typeorm';
import { User } from './entities/User';
import { Post } from './entities/Post';
export const AppDataSource = new DataSource({
type: 'postgres',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
username: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
database: process.env.DB_NAME || 'myapp',
entities: [User, Post],
migrations: ['src/migrations/*.ts'],
synchronize: false, // ❌ 生产环境必须为 false
logging: process.env.NODE_ENV === 'development',
});Entity 定义示例
// src/entities/User.ts
import {
Entity, PrimaryGeneratedColumn, Column,
CreateDateColumn, UpdateDateColumn, Index, OneToMany
} from 'typeorm';
import { Post } from './Post';
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', length: 255, unique: true })
@Index('idx_users_email')
email: string;
@Column({ type: 'varchar', length: 100 })
name: string;
@Column({ type: 'text', nullable: true })
avatarUrl: string | null;
@Column({ type: 'boolean', default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
}生成迁移
# 使用 ts-node 运行(需要 package.json 中配置脚本)
# package.json:
# "scripts": {
# "typeorm": "typeorm-ts-node-commonjs",
# "migration:generate": "npm run typeorm -- migration:generate -d src/data-source.ts",
# "migration:run": "npm run typeorm -- migration:run -d src/data-source.ts",
# "migration:revert": "npm run typeorm -- migration:revert -d src/data-source.ts"
# }
npm run migration:generate -- src/migrations/AddUserAvatar生成的迁移文件
// src/migrations/1718452800000-AddUserAvatar.ts
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUserAvatar1718452800000 implements MigrationInterface {
name = 'AddUserAvatar1718452800000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "users" ADD "avatar_url" text`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "users" DROP COLUMN "avatar_url"`
);
}
}5.2 TypeORM 迁移注意事项
// ❌ 常见错误:在生产环境使用 synchronize: true
const dataSource = new DataSource({
synchronize: true, // 这会自动修改数据库 Schema,可能导致数据丢失!
});
// ✅ 正确做法:生产环境只使用迁移
const dataSource = new DataSource({
synchronize: false,
migrationsRun: true, // 应用启动时自动运行迁移(可选)
});// ❌ 常见错误:migration:generate 生成了意外的变更
// 原因:Entity 与数据库不同步(可能有人手动修改了数据库)
// 解决:先用 schema:log 查看差异,确认后再生成
// ✅ 安全流程
// 1. 检查当前迁移状态
// npm run typeorm -- migration:show -d src/data-source.ts
// 2. 生成迁移(仔细审查生成的 SQL)
// npm run migration:generate -- src/migrations/MyChange
// 3. 审查生成的文件(重点检查 up 和 down 方法)
// 4. 在测试环境运行
// npm run migration:run
// 5. 验证数据完整性
// 6. 部署到生产5.3 TypeORM 迁移提示词模板
你是一位 TypeORM 专家。请帮我编写安全的 TypeORM 迁移。
## 当前 Entity
[粘贴当前 TypeORM Entity 代码]
## 需求变更
[描述需要的变更]
## 要求
1. 使用 QueryRunner API 编写 up() 和 down() 方法
2. down() 必须完整可逆
3. 对于数据迁移,使用事务包裹
4. 大表操作需要分批处理
5. 考虑 PostgreSQL/MySQL 的差异
## 输出
1. 完整的迁移类(implements MigrationInterface)
2. 对应的 Entity 变更
3. 部署注意事项6. Diesel:Rust 生态的编译时安全迁移
6.1 Diesel 迁移核心命令
Diesel 采用 SQL-first 方式:开发者手写 up.sql 和 down.sql,Diesel 在编译时验证 Schema 与代码的一致性。
| 命令 | 用途 | 环境 | 安全级别 |
|---|---|---|---|
diesel migration generate name | 创建迁移目录(up.sql + down.sql) | 开发 | ✅ 只创建空文件 |
diesel migration run | 执行所有未运行的迁移 | 开发/生产 | ⚠️ 执行 SQL |
diesel migration revert | 回滚最近一次迁移 | 生产 | ⚠️ 需确认回滚安全 |
diesel migration redo | 回滚并重新执行最近一次迁移 | 开发 | ⚠️ 用于测试迁移 |
diesel migration pending | 列出未执行的迁移 | 任何 | ✅ 只读操作 |
diesel migration list | 列出所有迁移及其状态 | 任何 | ✅ 只读操作 |
diesel print-schema | 从数据库生成 schema.rs | 开发 | ✅ 只读操作 |
diesel setup | 创建数据库并运行所有迁移 | 开发 | ⚠️ 创建数据库 |
Diesel 迁移文件结构
project/
├── migrations/
│ ├── 00000000000000_diesel_initial_setup/
│ │ ├── up.sql
│ │ └── down.sql
│ ├── 2025-06-15-120000_create_users/
│ │ ├── up.sql
│ │ └── down.sql
│ └── 2025-06-15-130000_create_posts/
│ ├── up.sql
│ └── down.sql
├── src/
│ ├── schema.rs # 自动生成(diesel print-schema)
│ └── models.rs # 手动编写的 Rust 结构体
├── diesel.toml
└── Cargo.toml迁移 SQL 示例
-- migrations/2025-06-15-120000_create_users/up.sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
avatar_url TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created_at ON users(created_at);-- migrations/2025-06-15-120000_create_users/down.sql
DROP TABLE IF EXISTS users;添加新字段的迁移
-- migrations/2025-06-16-100000_add_user_avatar/up.sql
ALTER TABLE users ADD COLUMN avatar_url TEXT;
ALTER TABLE users ADD COLUMN avatar_updated_at TIMESTAMP;
CREATE INDEX idx_users_avatar_updated ON users(avatar_updated_at)
WHERE avatar_url IS NOT NULL; -- 部分索引:只索引有头像的用户-- migrations/2025-06-16-100000_add_user_avatar/down.sql
DROP INDEX IF EXISTS idx_users_avatar_updated;
ALTER TABLE users DROP COLUMN IF EXISTS avatar_updated_at;
ALTER TABLE users DROP COLUMN IF EXISTS avatar_url;自动生成 schema.rs
# 运行迁移后,自动更新 schema.rs
diesel migration run
diesel print-schema > src/schema.rs// src/schema.rs(自动生成)
diesel::table! {
users (id) {
id -> Uuid,
email -> Varchar,
name -> Varchar,
avatar_url -> Nullable<Text>,
is_active -> Bool,
created_at -> Timestamp,
updated_at -> Timestamp,
}
}
diesel::table! {
posts (id) {
id -> Uuid,
title -> Varchar,
content -> Nullable<Text>,
author_id -> Uuid,
published_at -> Nullable<Timestamp>,
created_at -> Timestamp,
}
}
diesel::joinable!(posts -> users (author_id));
diesel::allow_tables_to_appear_in_same_query!(users, posts);6.2 Diesel 的编译时安全优势
Diesel 的核心优势是编译时 Schema 验证——如果迁移 SQL 与 Rust 代码不匹配,编译直接报错:
// src/models.rs
use diesel::prelude::*;
use uuid::Uuid;
use chrono::NaiveDateTime;
#[derive(Queryable, Selectable, Debug)]
#[diesel(table_name = crate::schema::users)]
pub struct User {
pub id: Uuid,
pub email: String,
pub name: String,
pub avatar_url: Option<String>,
pub is_active: bool,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
#[derive(Insertable)]
#[diesel(table_name = crate::schema::users)]
pub struct NewUser<'a> {
pub email: &'a str,
pub name: &'a str,
pub avatar_url: Option<&'a str>,
}
// 如果 schema.rs 中没有 avatar_url 字段,
// 这里会编译报错:no field `avatar_url` on type `users::table`
// ✅ 编译时就能发现迁移遗漏!6.3 Diesel 迁移提示词模板
你是一位 Rust/Diesel 专家。请帮我编写安全的 Diesel 迁移 SQL。
## 当前 schema.rs
[粘贴当前 schema.rs 内容]
## 需求变更
[描述需要的变更]
## 数据库类型
[PostgreSQL/MySQL/SQLite]
## 要求
1. 编写 up.sql 和 down.sql
2. down.sql 必须完整可逆
3. 使用 IF EXISTS / IF NOT EXISTS 防止重复执行
4. 对于 PostgreSQL,索引创建考虑使用 CONCURRENTLY
5. 新增 NOT NULL 列必须提供 DEFAULT 值或分步迁移
6. 提供对应的 Rust Model 结构体更新
## 输出
1. up.sql 内容
2. down.sql 内容
3. 更新后的 schema.rs 片段
4. 更新后的 models.rs 片段
5. 部署注意事项7. Django Migrations:成熟稳定的 Model-First 迁移
7.1 Django 迁移核心命令
Django 采用 Model-first 方式:开发者修改 Django Model,Django 自动检测变更并生成 Python 迁移文件。
| 命令 | 用途 | 环境 | 安全级别 |
|---|---|---|---|
python manage.py makemigrations | 检测 Model 变更,生成迁移文件 | 开发 | ✅ 只生成文件 |
python manage.py makemigrations --empty app | 创建空白迁移(用于数据迁移) | 开发 | ✅ 完全可控 |
python manage.py migrate | 执行所有未运行的迁移 | 开发/生产 | ⚠️ 执行 SQL |
python manage.py migrate app 000X | 回滚到指定迁移版本 | 生产 | ⚠️ 需确认回滚安全 |
python manage.py showmigrations | 显示所有迁移及其状态 | 任何 | ✅ 只读操作 |
python manage.py sqlmigrate app 000X | 显示迁移对应的 SQL(不执行) | 审查 | ✅ 只读操作 |
python manage.py migrate --plan | 显示将要执行的迁移计划 | 审查 | ✅ 只读操作 |
Django Model 示例
# myapp/models.py
from django.db import models
import uuid
class User(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
email = models.EmailField(max_length=255, unique=True, db_index=True)
name = models.CharField(max_length=100)
avatar_url = models.URLField(blank=True, null=True)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'users'
indexes = [
models.Index(fields=['created_at'], name='idx_users_created_at'),
models.Index(fields=['email'], name='idx_users_email'),
]
class Post(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(max_length=255)
content = models.TextField(blank=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
published_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'posts'
indexes = [
models.Index(fields=['author'], name='idx_posts_author'),
models.Index(fields=['published_at'], name='idx_posts_published'),
]生成迁移
# 生成迁移文件
python manage.py makemigrations myapp
# 查看生成的 SQL(审查用)
python manage.py sqlmigrate myapp 0002
# 执行迁移
python manage.py migrate生成的迁移文件
# myapp/migrations/0002_user_avatar_url.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('myapp', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='user',
name='avatar_url',
field=models.URLField(blank=True, null=True),
),
]7.2 Django 安全迁移模式
两阶段删除字段(PostHog 模式)
PostHog 团队总结的安全 Django 迁移规则:永远不要在运行中的代码仍然引用某个字段时删除它。
# ❌ 危险:一步删除字段
class Migration(migrations.Migration):
operations = [
migrations.RemoveField(
model_name='user',
name='old_field',
),
]
# 如果旧版本代码还在运行,会导致 500 错误!
# ✅ 安全:两阶段删除
# 阶段 1(版本 N):从代码中移除对 old_field 的所有引用
# 阶段 2(版本 N+1):删除数据库字段
class Migration(migrations.Migration):
operations = [
migrations.RemoveField(
model_name='user',
name='old_field',
),
]Django 数据迁移
# myapp/migrations/0003_migrate_status_data.py
from django.db import migrations
def migrate_status_forward(apps, schema_editor):
"""将 is_active 布尔值迁移到 status 枚举"""
User = apps.get_model('myapp', 'User')
# 分批处理,避免内存溢出
batch_size = 1000
users = User.objects.all()
for i in range(0, users.count(), batch_size):
batch = users[i:i + batch_size]
for user in batch:
user.status = 'active' if user.is_active else 'inactive'
User.objects.bulk_update(batch, ['status'], batch_size=batch_size)
def migrate_status_backward(apps, schema_editor):
"""回滚:将 status 枚举迁移回 is_active 布尔值"""
User = apps.get_model('myapp', 'User')
batch_size = 1000
users = User.objects.all()
for i in range(0, users.count(), batch_size):
batch = users[i:i + batch_size]
for user in batch:
user.is_active = user.status == 'active'
User.objects.bulk_update(batch, ['is_active'], batch_size=batch_size)
class Migration(migrations.Migration):
dependencies = [
('myapp', '0002_user_avatar_url'),
]
operations = [
migrations.RunPython(
migrate_status_forward,
migrate_status_backward, # ✅ 必须提供回滚函数
),
]7.3 Django 迁移提示词模板
你是一位 Django 专家。请帮我编写安全的 Django 迁移。
## 当前 Model
[粘贴当前 Django Model 代码]
## 需求变更
[描述需要的变更]
## 部署方式
[滚动部署/蓝绿部署/单机部署]
## 要求
1. 遵循两阶段删除原则(先移除代码引用,再删除字段)
2. 数据迁移使用 RunPython,必须提供 reverse 函数
3. 大表数据迁移使用分批处理(batch_size=1000)
4. 新增 NOT NULL 字段必须提供 default 或分步迁移
5. 考虑滚动部署时新旧代码并存的兼容性
6. 使用 sqlmigrate 预览 SQL 并评估锁表风险
## 输出
1. 完整的迁移文件(operations 列表)
2. 对应的 Model 变更
3. 部署步骤(先迁移还是先部署代码?)
4. 回滚步骤8. AI 辅助迁移安全审查
8.1 迁移安全审查框架
AI 辅助迁移审查的核心是识别四类风险:破坏性操作、锁表风险、数据丢失风险和向后兼容性问题。
┌─────────────────────────────────────────────────────────────────┐
│ 迁移安全审查四维模型 │
├────────────────┬────────────────────────────────────────────────┤
│ │ │
│ 🔴 破坏性操作 │ DROP TABLE / DROP COLUMN / TRUNCATE │
│ (Destructive) │ ALTER COLUMN TYPE(缩小范围) │
│ │ DROP INDEX / DROP CONSTRAINT │
│ │ │
├────────────────┼────────────────────────────────────────────────┤
│ │ │
│ 🟡 锁表风险 │ ALTER TABLE ADD COLUMN ... NOT NULL │
│ (Lock Risk) │ ALTER TABLE ADD COLUMN ... DEFAULT(MySQL<8.0)│
│ │ CREATE INDEX(非 CONCURRENTLY) │
│ │ ALTER TABLE ... ADD CONSTRAINT │
│ │ RENAME TABLE / RENAME COLUMN │
│ │ │
├────────────────┼────────────────────────────────────────────────┤
│ │ │
│ 🟠 数据丢失 │ ALTER COLUMN TYPE(精度降低) │
│ (Data Loss) │ ALTER COLUMN SET NOT NULL(含 NULL 数据) │
│ │ UPDATE ... SET(不可逆数据转换) │
│ │ DELETE FROM(数据清理) │
│ │ │
├────────────────┼────────────────────────────────────────────────┤
│ │ │
│ 🔵 兼容性 │ RENAME COLUMN(旧代码引用旧名) │
│ (Compat) │ DROP DEFAULT(旧代码依赖默认值) │
│ │ ADD NOT NULL(旧代码不写入该字段) │
│ │ CHANGE ENUM VALUES(旧代码使用旧值) │
│ │ │
└────────────────┴────────────────────────────────────────────────┘8.2 破坏性操作检测清单
| 操作 | 风险级别 | 检测方法 | 安全替代方案 |
|---|---|---|---|
DROP TABLE | 🔴 极高 | SQL 关键字匹配 | 先重命名为 _deprecated_,观察 7 天后删除 |
DROP COLUMN | 🔴 极高 | SQL 关键字匹配 | 两阶段:先停止写入 → 下个版本删除 |
TRUNCATE TABLE | 🔴 极高 | SQL 关键字匹配 | 使用 DELETE WHERE + 条件删除 |
ALTER COLUMN TYPE(缩小) | 🟡 高 | 比较新旧类型范围 | 新增列 → 数据迁移 → 删除旧列 |
ALTER COLUMN SET NOT NULL | 🟡 高 | 检查是否有 NULL 数据 | 先填充默认值 → 再设置 NOT NULL |
DROP INDEX | 🟡 中 | SQL 关键字匹配 | 确认无查询依赖该索引后删除 |
DROP CONSTRAINT | 🟡 中 | SQL 关键字匹配 | 确认业务逻辑不依赖该约束 |
RENAME TABLE | 🟡 中 | SQL 关键字匹配 | 创建视图别名 → 迁移代码 → 删除视图 |
RENAME COLUMN | 🟡 中 | SQL 关键字匹配 | 新增列 → 数据同步 → 迁移代码 → 删除旧列 |
8.3 锁表风险分析
不同数据库对 ALTER TABLE 的锁行为差异很大:
PostgreSQL 锁表行为
| 操作 | 锁类型 | 阻塞读 | 阻塞写 | 大表影响 |
|---|---|---|---|---|
ADD COLUMN(nullable,无 default) | AccessExclusiveLock | ❌ 极短 | ❌ 极短 | ✅ 安全(仅修改元数据) |
ADD COLUMN ... DEFAULT val(PG 11+) | AccessExclusiveLock | ❌ 极短 | ❌ 极短 | ✅ 安全(仅修改元数据) |
ADD COLUMN ... NOT NULL DEFAULT val(PG 11+) | AccessExclusiveLock | ❌ 极短 | ❌ 极短 | ✅ 安全(仅修改元数据) |
ALTER COLUMN TYPE | AccessExclusiveLock | ✅ 阻塞 | ✅ 阻塞 | ❌ 重写整表 |
ALTER COLUMN SET NOT NULL | AccessExclusiveLock | ❌ 极短 | ❌ 极短 | ⚠️ 需全表扫描验证(PG 12+ 可用 CHECK 约束优化) |
CREATE INDEX | ShareLock | ❌ | ✅ 阻塞 | ❌ 长时间阻塞写入 |
CREATE INDEX CONCURRENTLY | ShareUpdateExclusiveLock | ❌ | ❌ | ✅ 安全(不阻塞写入) |
DROP COLUMN | AccessExclusiveLock | ❌ 极短 | ❌ 极短 | ✅ 安全(仅标记删除) |
ADD CONSTRAINT ... FOREIGN KEY | ShareRowExclusiveLock | ❌ | ✅ 阻塞 | ❌ 需全表扫描验证 |
ADD CONSTRAINT ... NOT VALID | ShareRowExclusiveLock | ❌ | ❌ 极短 | ✅ 安全(跳过验证) |
VALIDATE CONSTRAINT | ShareUpdateExclusiveLock | ❌ | ❌ | ✅ 安全(不阻塞写入) |
MySQL 锁表行为
| 操作 | 算法 | 阻塞 DML | 大表影响 |
|---|---|---|---|
ADD COLUMN(末尾) | INSTANT(MySQL 8.0+) | ❌ | ✅ 安全 |
ADD COLUMN(中间) | INPLACE | ⚠️ 短暂 | ⚠️ 需重建表 |
ALTER COLUMN TYPE | COPY | ✅ 阻塞 | ❌ 重写整表 |
CREATE INDEX | INPLACE | ❌ | ⚠️ 耗时但不阻塞 |
DROP COLUMN | INPLACE | ⚠️ 短暂 | ⚠️ 需重建表 |
ADD FOREIGN KEY | INPLACE | ❌ | ⚠️ 需扫描验证 |
8.4 AI 迁移安全审查提示词模板
通用迁移审查提示词
你是一位资深数据库工程师和迁移安全专家。请对以下迁移文件进行全面安全审查。
## 迁移文件
[粘贴迁移文件内容(SQL 或 ORM 迁移脚本)]
## 环境信息
- 数据库类型:[PostgreSQL 16 / MySQL 8.4 / SQLite]
- 受影响表的行数:[表名: 约 X 行]
- 部署方式:[滚动部署 / 蓝绿部署 / 停机部署]
- 是否有读副本:[是/否]
- 当前 QPS:[约 X 请求/秒]
## 审查维度
### 1. 破坏性操作检测
- 是否包含 DROP TABLE / DROP COLUMN / TRUNCATE?
- 是否有不可逆的数据类型变更?
- 是否删除了索引或约束?
### 2. 锁表风险评估
- ALTER TABLE 操作是否会获取排他锁?
- 锁持续时间预估(基于表大小)?
- 是否可以使用 CONCURRENTLY / INSTANT / INPLACE 替代?
### 3. 数据完整性
- 新增 NOT NULL 列是否有默认值?
- 数据类型变更是否可能导致数据截断?
- 外键约束是否正确设置了级联行为?
### 4. 向后兼容性
- 旧版本代码是否能与新 Schema 兼容?
- 是否需要多步迁移(expand-contract 模式)?
- 回滚后旧代码是否能正常工作?
### 5. 性能影响
- 迁移执行时间预估?
- 是否需要分批执行?
- 对在线查询的影响?
## 输出格式
对每个发现的问题:
- 🔴 高风险 / 🟡 中风险 / 🟢 低风险
- 问题描述
- 影响范围
- 修复建议(提供具体的替代 SQL)
- 回滚方案
最后提供:
- 总体风险评级
- 推荐的执行顺序
- 执行前检查清单
- 执行后验证清单大表迁移专用提示词
你是一位数据库性能专家。我需要在一个大表上执行 Schema 变更,
请帮我设计安全的迁移方案。
## 表信息
- 表名:[表名]
- 行数:[约 X 百万行]
- 表大小:[约 X GB]
- 数据库:[PostgreSQL / MySQL]
- 当前 QPS:[约 X 请求/秒]
- 维护窗口:[有/无,如有则为 X 小时]
## 需要执行的变更
[描述 Schema 变更]
## 约束条件
- 最大允许停机时间:[0 / X 分钟]
- 最大允许性能降级:[X%]
- 是否可以分批执行:[是/否]
## 请提供
1. 分步迁移方案(每步的 SQL 和预估时间)
2. 每步的锁表分析
3. 监控指标(执行期间需要关注什么)
4. 回滚方案(每步的回滚 SQL)
5. 执行脚本(可直接运行的 SQL 脚本,包含进度输出)9. 零停机迁移模式
9.1 Expand-Contract 模式(扩展-收缩)
Expand-Contract 是零停机迁移的核心模式:先扩展 Schema(添加新结构),迁移数据和代码,最后收缩(删除旧结构)。
┌─────────────────────────────────────────────────────────────────┐
│ Expand-Contract 迁移模式 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 阶段 1:Expand(扩展) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 添加新列/新表 │ │
│ │ • 新列设为 nullable 或有 default │ │
│ │ • 不删除任何旧结构 │ │
│ │ • 旧代码继续正常工作 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ 阶段 2:Migrate(迁移数据 + 代码) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 后台任务迁移历史数据到新结构 │ │
│ │ • 部署新代码(同时写入新旧结构) │ │
│ │ • 使用触发器或应用层双写保持同步 │ │
│ │ • 验证新旧数据一致性 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ 阶段 3:Contract(收缩) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 部署只读新结构的代码 │ │
│ │ • 确认所有服务已切换到新结构 │ │
│ │ • 等待回滚窗口过期(通常 7-14 天) │ │
│ │ • 删除旧列/旧表 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘实战示例:重命名列(email → email_address)
-- ❌ 危险:直接重命名(会导致旧代码崩溃)
ALTER TABLE users RENAME COLUMN email TO email_address;
-- ✅ 安全:Expand-Contract 三阶段
-- 阶段 1:Expand(添加新列)
ALTER TABLE users ADD COLUMN email_address VARCHAR(255);
UPDATE users SET email_address = email; -- 迁移历史数据
-- 添加触发器保持同步
CREATE OR REPLACE FUNCTION sync_email_columns()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
IF NEW.email IS DISTINCT FROM NEW.email_address THEN
IF NEW.email IS NOT NULL AND NEW.email_address IS NULL THEN
NEW.email_address := NEW.email;
ELSIF NEW.email_address IS NOT NULL THEN
NEW.email := NEW.email_address;
END IF;
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_sync_email
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION sync_email_columns();
-- 阶段 2:Migrate(部署新代码,读写 email_address)
-- 新代码同时写入 email 和 email_address
-- 读取优先使用 email_address
-- 阶段 3:Contract(确认所有服务已切换后)
DROP TRIGGER trg_sync_email ON users;
DROP FUNCTION sync_email_columns();
ALTER TABLE users DROP COLUMN email;
-- 可选:添加唯一约束
ALTER TABLE users ADD CONSTRAINT uq_users_email_address UNIQUE (email_address);
ALTER TABLE users ALTER COLUMN email_address SET NOT NULL;9.2 蓝绿部署迁移策略
┌─────────────────────────────────────────────────────────────────┐
│ 蓝绿部署 + 数据库迁移 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 步骤 1:当前状态 │
│ ┌──────────┐ ┌──────────┐ │
│ │ 蓝(活跃) │────→│ 数据库 │ │
│ │ App v1 │ │ Schema v1│ │
│ └──────────┘ └──────────┘ │
│ │
│ 步骤 2:执行向前兼容的迁移 │
│ ┌──────────┐ ┌──────────┐ │
│ │ 蓝(活跃) │────→│ 数据库 │ ← Schema v2(兼容 v1 代码) │
│ │ App v1 │ │ Schema v2│ │
│ └──────────┘ └──────────┘ │
│ │
│ 步骤 3:部署新代码到绿环境 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 蓝(活跃) │────→│ 数据库 │←────│ 绿(待命) │ │
│ │ App v1 │ │ Schema v2│ │ App v2 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ 步骤 4:切换流量到绿环境 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 蓝(待命) │ │ 数据库 │←────│ 绿(活跃) │ │
│ │ App v1 │ │ Schema v2│ │ App v2 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ 关键约束:Schema v2 必须同时兼容 App v1 和 App v2 │
│ │
└─────────────────────────────────────────────────────────────────┘蓝绿部署迁移规则
✅ 允许的迁移操作(向前兼容):
• ADD COLUMN(nullable 或有 default)
• ADD TABLE
• ADD INDEX(CONCURRENTLY)
• ADD CONSTRAINT ... NOT VALID
❌ 禁止的迁移操作(破坏向前兼容):
• DROP COLUMN(旧代码还在读)
• DROP TABLE(旧代码还在用)
• RENAME COLUMN/TABLE
• ALTER COLUMN TYPE(可能不兼容)
• ALTER COLUMN SET NOT NULL(旧代码可能写入 NULL)9.3 回滚策略与 Down Migration 验证
回滚决策树
迁移出现问题?
│
├── 数据未被修改?
│ ├── 是 → 直接执行 down migration
│ └── 否 → 继续评估 ↓
│
├── down migration 是否安全?
│ ├── 是(仅删除新增的列/表)→ 执行 down migration
│ └── 否(涉及数据恢复)→ 继续评估 ↓
│
├── 有数据库备份?
│ ├── 是 → 评估从备份恢复的时间
│ │ ├── 可接受 → 从备份恢复
│ │ └── 不可接受 → 手动修复
│ └── 否 → 手动修复
│
└── 手动修复
├── 编写修复 SQL
├── 在测试环境验证
└── 在生产环境执行Down Migration 验证脚本
#!/bin/bash
# verify_migration_rollback.sh
# 在 CI 中验证每个迁移的 up + down 是否可逆
set -e
echo "=== 迁移回滚验证 ==="
# 1. 创建测试数据库
createdb migration_test_db
# 2. 应用所有迁移
echo ">>> 应用所有迁移..."
DATABASE_URL="postgresql://localhost/migration_test_db" \
npx prisma migrate deploy # 或对应的 ORM 命令
# 3. 记录当前 Schema
pg_dump --schema-only migration_test_db > schema_after_up.sql
# 4. 插入测试数据
echo ">>> 插入测试数据..."
psql migration_test_db -f test_data.sql
# 5. 回滚最近一次迁移
echo ">>> 回滚最近一次迁移..."
# Alembic: alembic downgrade -1
# Django: python manage.py migrate app 000X
# Diesel: diesel migration revert
# 6. 重新应用迁移
echo ">>> 重新应用迁移..."
# 对应的 migrate 命令
# 7. 比较 Schema
pg_dump --schema-only migration_test_db > schema_after_redo.sql
diff schema_after_up.sql schema_after_redo.sql
if [ $? -eq 0 ]; then
echo "✅ 迁移回滚验证通过:up → down → up 结果一致"
else
echo "❌ 迁移回滚验证失败:Schema 不一致!"
exit 1
fi
# 8. 验证数据完整性
echo ">>> 验证数据完整性..."
psql migration_test_db -c "SELECT count(*) FROM users;" | grep -q "0" && {
echo "❌ 数据丢失!"
exit 1
}
echo "✅ 数据完整性验证通过"
# 9. 清理
dropdb migration_test_db
rm -f schema_after_up.sql schema_after_redo.sql
echo "=== 验证完成 ==="9.4 数据迁移脚本最佳实践
数据迁移(Data Migration)与 Schema 迁移不同,它涉及数据的转换和移动:
# Alembic 数据迁移最佳实践示例
"""migrate user addresses to separate table
Revision ID: mno345
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy import text
revision = 'mno345'
down_revision = 'jkl012'
def upgrade() -> None:
# 步骤 1:创建新表
op.create_table(
'addresses',
sa.Column('id', sa.UUID(), primary_key=True,
server_default=sa.text('gen_random_uuid()')),
sa.Column('user_id', sa.UUID(), sa.ForeignKey('users.id'),
nullable=False),
sa.Column('street', sa.String(255), nullable=False),
sa.Column('city', sa.String(100), nullable=False),
sa.Column('country', sa.String(100), nullable=False),
sa.Column('created_at', sa.DateTime(),
server_default=sa.text('NOW()'), nullable=False),
)
# 步骤 2:分批迁移数据(避免长事务和内存溢出)
conn = op.get_bind()
batch_size = 5000
offset = 0
while True:
result = conn.execute(text("""
INSERT INTO addresses (user_id, street, city, country)
SELECT id, address_street, address_city, address_country
FROM users
WHERE address_street IS NOT NULL
ORDER BY id
LIMIT :batch_size OFFSET :offset
RETURNING id
"""), {"batch_size": batch_size, "offset": offset})
rows = result.fetchall()
if not rows:
break
offset += batch_size
# 可选:输出进度
print(f" 已迁移 {offset} 条地址记录...")
# 步骤 3:创建索引(在数据迁移完成后)
op.create_index('idx_addresses_user_id', 'addresses', ['user_id'])
# 注意:不要在这里删除旧列!留到下一个迁移版本
def downgrade() -> None:
# 回滚:将数据迁移回 users 表
conn = op.get_bind()
conn.execute(text("""
UPDATE users u
SET address_street = a.street,
address_city = a.city,
address_country = a.country
FROM addresses a
WHERE a.user_id = u.id
"""))
op.drop_table('addresses')10. CI/CD 集成:迁移安全门
10.1 GitHub Actions 迁移安全流水线
# .github/workflows/migration-safety.yml
name: Migration Safety Check
on:
pull_request:
paths:
- 'prisma/migrations/**'
- 'drizzle/**'
- 'alembic/versions/**'
- 'migrations/**'
- 'src/migrations/**'
jobs:
migration-review:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: test_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # 需要完整历史来比较变更
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install dependencies
run: npm ci
# 步骤 1:检测迁移文件变更
- name: Detect migration changes
id: detect
run: |
CHANGED_FILES=$(git diff --name-only origin/main...HEAD)
echo "changed_files<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGED_FILES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# 检查是否有迁移文件变更
if echo "$CHANGED_FILES" | grep -qE '(migrations/|drizzle/)'; then
echo "has_migrations=true" >> $GITHUB_OUTPUT
else
echo "has_migrations=false" >> $GITHUB_OUTPUT
fi
# 步骤 2:破坏性操作检测
- name: Check for destructive operations
if: steps.detect.outputs.has_migrations == 'true'
run: |
echo "=== 检查破坏性操作 ==="
MIGRATION_FILES=$(git diff --name-only origin/main...HEAD | grep -E '\.(sql|py|ts)$' | grep -iE '(migration|alembic|drizzle)')
FOUND_ISSUES=false
for file in $MIGRATION_FILES; do
if [ -f "$file" ]; then
echo "检查文件: $file"
# 检测 DROP 操作
if grep -inE 'DROP\s+(TABLE|COLUMN|INDEX|CONSTRAINT)' "$file"; then
echo "🔴 发现 DROP 操作: $file"
FOUND_ISSUES=true
fi
# 检测 TRUNCATE 操作
if grep -inE 'TRUNCATE\s+TABLE' "$file"; then
echo "🔴 发现 TRUNCATE 操作: $file"
FOUND_ISSUES=true
fi
# 检测 ALTER COLUMN TYPE
if grep -inE 'ALTER\s+COLUMN.*TYPE' "$file"; then
echo "🟡 发现 ALTER COLUMN TYPE: $file"
FOUND_ISSUES=true
fi
# 检测非 CONCURRENTLY 的 CREATE INDEX
if grep -inE 'CREATE\s+INDEX(?!\s+CONCURRENTLY)' "$file"; then
echo "🟡 发现非 CONCURRENTLY 的 CREATE INDEX: $file"
fi
# 检测 NOT NULL 无 DEFAULT
if grep -inE 'NOT\s+NULL' "$file" | grep -ivE 'DEFAULT'; then
echo "🟡 发现 NOT NULL 无 DEFAULT: $file"
fi
fi
done
if [ "$FOUND_ISSUES" = true ]; then
echo ""
echo "⚠️ 发现潜在危险操作,请确认是否安全。"
echo "如果确认安全,请在 PR 描述中添加 [migration-approved] 标签。"
# 不直接失败,而是添加 PR 评论
fi
# 步骤 3:迁移可逆性验证
- name: Verify migration reversibility
if: steps.detect.outputs.has_migrations == 'true'
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
run: |
echo "=== 验证迁移可逆性 ==="
# 应用所有迁移
npx prisma migrate deploy # 或对应的 ORM 命令
# 记录 Schema
pg_dump --schema-only -h localhost -U postgres test_db > schema_v1.sql
# 注意:回滚验证取决于 ORM
# Prisma 不原生支持 down migration
# Alembic: alembic downgrade -1 && alembic upgrade head
# Django: python manage.py migrate app 000X && python manage.py migrate
echo "✅ 迁移应用成功"
# 步骤 4:生成迁移审查报告
- name: Generate migration review report
if: steps.detect.outputs.has_migrations == 'true'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const { execSync } = require('child_process');
// 获取变更的迁移文件
const changedFiles = execSync(
'git diff --name-only origin/main...HEAD'
).toString().split('\n').filter(f =>
f.match(/(migration|alembic|drizzle)/i) && f.match(/\.(sql|py|ts)$/)
);
let report = '## 🗄️ 数据库迁移审查报告\n\n';
report += `发现 ${changedFiles.length} 个迁移文件变更:\n\n`;
for (const file of changedFiles) {
if (fs.existsSync(file)) {
const content = fs.readFileSync(file, 'utf8');
report += `### \`${file}\`\n\n`;
// 检测风险
const risks = [];
if (/DROP\s+(TABLE|COLUMN)/i.test(content))
risks.push('🔴 包含 DROP 操作');
if (/TRUNCATE/i.test(content))
risks.push('🔴 包含 TRUNCATE 操作');
if (/ALTER\s+COLUMN.*TYPE/i.test(content))
risks.push('🟡 包含列类型变更');
if (/CREATE\s+INDEX(?!\s+CONCURRENTLY)/i.test(content))
risks.push('🟡 CREATE INDEX 未使用 CONCURRENTLY');
if (/NOT\s+NULL/i.test(content) && !/DEFAULT/i.test(content))
risks.push('🟡 NOT NULL 列可能缺少 DEFAULT');
if (risks.length > 0) {
report += '**风险项:**\n';
risks.forEach(r => report += `- ${r}\n`);
} else {
report += '✅ 未发现明显风险\n';
}
report += '\n';
}
}
report += '\n---\n';
report += '> 🤖 此报告由 CI 自动生成。';
report += '请数据库负责人审查后批准。\n';
// 发布 PR 评论
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: report
});
# 步骤 5:需要审批的迁移
- name: Require approval for dangerous migrations
if: steps.detect.outputs.has_migrations == 'true'
run: |
MIGRATION_FILES=$(git diff --name-only origin/main...HEAD | grep -iE '(migration|alembic|drizzle)')
HAS_DANGEROUS=false
for file in $MIGRATION_FILES; do
if [ -f "$file" ] && grep -qiE 'DROP\s+(TABLE|COLUMN)|TRUNCATE' "$file"; then
HAS_DANGEROUS=true
break
fi
done
if [ "$HAS_DANGEROUS" = true ]; then
# 检查 PR 描述是否包含审批标签
PR_BODY="${{ github.event.pull_request.body }}"
if echo "$PR_BODY" | grep -q '\[migration-approved\]'; then
echo "✅ 迁移已获批准"
else
echo "❌ 发现破坏性迁移操作,需要在 PR 描述中添加 [migration-approved] 标签"
exit 1
fi
fi10.2 Bytebase SQL Review CI 集成
Bytebase 提供专业的 SQL 审查 CI 集成,可以在 PR 中自动检查迁移 SQL 的安全性:
# .github/workflows/sql-review.yml
name: SQL Review
on:
pull_request:
paths:
- '**/*.sql'
jobs:
sql-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Bytebase SQL Review
uses: bytebase/sql-review-action@v1
with:
# SQL Review 规则配置
override-file-path: '.github/sql-review-rules.yml'
# 数据库类型
database-type: 'POSTGRES'# .github/sql-review-rules.yml
# Bytebase SQL Review 规则配置
rules:
# 禁止 DROP TABLE
- type: statement.disallow-drop-table
level: ERROR
# 禁止 DROP COLUMN(需要审批)
- type: statement.disallow-drop-column
level: WARNING
# 要求 CREATE INDEX CONCURRENTLY
- type: index.create-concurrently
level: ERROR
# NOT NULL 列必须有 DEFAULT
- type: column.require-default-for-not-null
level: ERROR
# 表必须有主键
- type: table.require-pk
level: ERROR
# 列命名规范(snake_case)
- type: naming.column
level: WARNING
payload:
format: "^[a-z][a-z0-9_]*$"
# 索引命名规范
- type: naming.index
level: WARNING
payload:
format: "^idx_[a-z][a-z0-9_]*$"
# 外键命名规范
- type: naming.foreign-key
level: WARNING
payload:
format: "^fk_[a-z][a-z0-9_]*$"10.3 Atlas 声明式迁移 CI/CD
Atlas 提供了现代化的声明式迁移 CI/CD 集成:
# .github/workflows/atlas-ci.yml
name: Atlas Migration CI
on:
pull_request:
paths:
- 'schema.hcl'
- 'migrations/**'
jobs:
atlas-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Atlas Lint
uses: ariga/atlas-action/migrate/lint@v1
with:
dir: 'file://migrations'
dev-url: 'docker://postgres/16/dev'
# 检测破坏性变更
# 检测数据依赖问题
# 验证迁移文件完整性11. Kiro Hooks:自动化迁移审查
11.1 迁移文件变更自动审查 Hook
利用 Kiro Hooks,可以在迁移文件被创建或修改时自动触发 AI 安全审查:
// .kiro/hooks/migration-review.json
{
"name": "Migration Safety Review",
"description": "当迁移文件被创建或修改时,自动进行安全审查",
"event": "fileCreated",
"filePatterns": [
"prisma/migrations/**/*.sql",
"drizzle/**/*.sql",
"alembic/versions/**/*.py",
"migrations/**/*.sql",
"migrations/**/*.py",
"src/migrations/**/*.ts"
],
"action": "askAgent",
"prompt": "请对这个新创建的迁移文件进行安全审查。检查以下风险:\n1. 破坏性操作(DROP TABLE/COLUMN/INDEX)\n2. 锁表风险(ALTER TABLE 在大表上)\n3. 数据丢失风险(NOT NULL 无 DEFAULT、类型缩小)\n4. 向后兼容性(旧代码是否能与新 Schema 兼容)\n5. 回滚可行性(down migration 是否完整)\n\n对每个发现的问题标注风险级别(🔴高/🟡中/🟢低)并提供修复建议。"
}11.2 Schema 文件变更触发迁移提醒 Hook
// .kiro/hooks/schema-change-reminder.json
{
"name": "Schema Change Migration Reminder",
"description": "当 Schema 文件被修改时,提醒生成迁移文件",
"event": "fileEdited",
"filePatterns": [
"prisma/schema.prisma",
"src/db/schema.ts",
"src/db/schema/*.ts",
"app/models.py",
"*/models.py"
],
"action": "askAgent",
"prompt": "检测到 Schema/Model 文件被修改。请:\n1. 分析变更内容\n2. 评估是否需要生成迁移文件\n3. 如果需要,提供生成迁移的命令\n4. 预估迁移的安全风险\n5. 如果涉及大表变更,建议使用 Expand-Contract 模式"
}11.3 迁移部署前检查 Hook
// .kiro/hooks/pre-deploy-migration-check.json
{
"name": "Pre-Deploy Migration Check",
"description": "部署前检查迁移文件的安全性和完整性",
"event": "userTriggered",
"action": "askAgent",
"prompt": "请执行部署前迁移检查:\n1. 列出所有待执行的迁移文件\n2. 检查每个迁移的安全性(破坏性操作、锁表风险)\n3. 验证迁移顺序是否正确\n4. 确认所有迁移都有对应的回滚方案\n5. 生成部署清单和执行顺序建议"
}11.4 Steering 规则:迁移安全规范
<!-- .kiro/steering/database-migration-safety.md -->
# 数据库迁移安全规范
## 迁移文件生成规则
1. **永远不要直接修改已应用的迁移文件**
- 已应用的迁移是不可变的历史记录
- 需要修改时,创建新的迁移文件
2. **每个迁移文件只做一件事**
- 不要在一个迁移中混合 Schema 变更和数据迁移
- Schema 变更和数据迁移应该是独立的迁移文件
3. **所有迁移必须可回滚**
- 每个 up migration 必须有对应的 down migration
- down migration 必须经过测试验证
4. **大表操作必须使用安全模式**
- PostgreSQL:CREATE INDEX CONCURRENTLY
- PostgreSQL:ADD CONSTRAINT ... NOT VALID + VALIDATE CONSTRAINT
- MySQL:使用 pt-online-schema-change 或 gh-ost
- 数据迁移:分批处理(batch_size ≤ 5000)
5. **破坏性操作必须使用 Expand-Contract 模式**
- DROP COLUMN:先停止写入 → 下个版本删除
- RENAME COLUMN:新增列 → 数据同步 → 迁移代码 → 删除旧列
- ALTER COLUMN TYPE:新增列 → 数据转换 → 迁移代码 → 删除旧列
6. **新增 NOT NULL 列的安全步骤**
- 步骤 1:ADD COLUMN ... DEFAULT value(nullable)
- 步骤 2:UPDATE 填充历史数据(分批)
- 步骤 3:ALTER COLUMN SET NOT NULL
7. **迁移文件命名规范**
- 使用描述性名称:add_user_avatar、create_posts_table
- 不要使用通用名称:update、fix、change
## AI 生成迁移的审查清单
当 AI 生成迁移文件时,必须检查:
- [ ] 是否包含破坏性操作?
- [ ] 大表操作是否使用了安全模式?
- [ ] NOT NULL 列是否有 DEFAULT?
- [ ] 索引创建是否使用了 CONCURRENTLY?
- [ ] down migration 是否完整且可逆?
- [ ] 是否考虑了滚动部署的兼容性?
- [ ] 数据迁移是否使用了分批处理?
- [ ] 外键约束是否正确设置了级联行为?12. 跨 ORM 迁移安全对比
12.1 各 ORM 迁移安全特性对比
| 安全特性 | Prisma | Drizzle | Alembic | TypeORM | Diesel | Django |
|---|---|---|---|---|---|---|
| AI Safety Guardrails | ✅ v6.15+ | ❌ | ❌ | ❌ | ❌ | ❌ |
| 自动检测破坏性操作 | ✅ 警告 | ⚠️ 提示确认 | ❌ 需手动 | ❌ 需手动 | ❌ 需手动 | ⚠️ 部分检测 |
| Down Migration 自动生成 | ❌ 需手动 | ❌ 需手动 | ✅ autogenerate | ✅ 自动生成 | ❌ 需手动 | ✅ 自动生成 |
| 迁移文件可读性 | ✅ 纯 SQL | ✅ 纯 SQL | ⚠️ Python 代码 | ⚠️ TypeScript 代码 | ✅ 纯 SQL | ⚠️ Python 代码 |
| 编译时 Schema 验证 | ✅ 类型安全 | ✅ 类型安全 | ❌ 运行时 | ⚠️ 部分类型安全 | ✅ 编译时验证 | ❌ 运行时 |
| 迁移状态追踪 | ✅ _prisma_migrations 表 | ✅ __drizzle_migrations 表 | ✅ alembic_version 表 | ✅ migrations 表 | ✅ __diesel_schema_migrations 表 | ✅ django_migrations 表 |
| CI/CD 集成 | ✅ migrate deploy | ✅ migrate | ✅ upgrade head | ✅ migration:run | ✅ migration run | ✅ migrate |
| 数据迁移支持 | ⚠️ 需手写 SQL | ⚠️ 需手写 SQL | ✅ RunPython | ✅ QueryRunner | ⚠️ 需手写 SQL | ✅ RunPython |
| 多数据库支持 | PostgreSQL, MySQL, SQLite, SQL Server, CockroachDB | PostgreSQL, MySQL, SQLite | 所有 SQLAlchemy 支持的数据库 | PostgreSQL, MySQL, SQLite, SQL Server, Oracle | PostgreSQL, MySQL, SQLite | PostgreSQL, MySQL, SQLite, Oracle |
12.2 各 ORM 迁移命令速查表
┌──────────────────────────────────────────────────────────────────────────┐
│ 迁移命令速查表 │
├──────────┬───────────────────────────────────────────────────────────────┤
│ 操作 │ 各 ORM 对应命令 │
├──────────┼───────────────────────────────────────────────────────────────┤
│ │ Prisma: npx prisma migrate dev --name <name> │
│ 生成迁移 │ Drizzle: npx drizzle-kit generate │
│ │ Alembic: alembic revision --autogenerate -m "<msg>" │
│ │ TypeORM: npx typeorm migration:generate src/migrations/Name │
│ │ Diesel: diesel migration generate <name> │
│ │ Django: python manage.py makemigrations │
├──────────┼───────────────────────────────────────────────────────────────┤
│ │ Prisma: npx prisma migrate deploy │
│ 应用迁移 │ Drizzle: npx drizzle-kit migrate │
│ │ Alembic: alembic upgrade head │
│ │ TypeORM: npx typeorm migration:run -d src/data-source.ts │
│ │ Diesel: diesel migration run │
│ │ Django: python manage.py migrate │
├──────────┼───────────────────────────────────────────────────────────────┤
│ │ Prisma: npx prisma migrate resolve(手动标记) │
│ 回滚迁移 │ Drizzle: 手动执行回滚 SQL │
│ │ Alembic: alembic downgrade -1 │
│ │ TypeORM: npx typeorm migration:revert -d src/data-source.ts │
│ │ Diesel: diesel migration revert │
│ │ Django: python manage.py migrate <app> <migration_number> │
├──────────┼───────────────────────────────────────────────────────────────┤
│ │ Prisma: npx prisma migrate status │
│ 查看状态 │ Drizzle: npx drizzle-kit check │
│ │ Alembic: alembic current / alembic history │
│ │ TypeORM: npx typeorm migration:show -d src/data-source.ts │
│ │ Diesel: diesel migration list │
│ │ Django: python manage.py showmigrations │
├──────────┼───────────────────────────────────────────────────────────────┤
│ │ Prisma: npx prisma migrate diff │
│ 预览 SQL │ Drizzle: 查看生成的 .sql 文件 │
│ │ Alembic: alembic upgrade head --sql │
│ │ TypeORM: 查看生成的 .ts 文件中的 SQL │
│ │ Diesel: 查看 up.sql / down.sql │
│ │ Django: python manage.py sqlmigrate <app> <number> │
└──────────┴───────────────────────────────────────────────────────────────┘实战案例:电商平台用户表重构迁移
案例背景
一个电商平台需要对 users 表进行重构:
- 将
full_name拆分为first_name和last_name - 将内嵌的地址字段迁移到独立的
addresses表 - 添加
phone_verified字段 - 表当前有 500 万行数据
- 要求零停机部署
迁移方案设计(Expand-Contract 模式)
第一步:Expand — 添加新结构(迁移 1)
-- Migration 001: expand_user_table.sql
-- 安全级别:🟢 低风险(仅添加 nullable 列和新表)
-- 1. 添加新列(nullable,不影响现有代码)
ALTER TABLE users ADD COLUMN first_name VARCHAR(50);
ALTER TABLE users ADD COLUMN last_name VARCHAR(50);
ALTER TABLE users ADD COLUMN phone_verified BOOLEAN DEFAULT FALSE;
-- 2. 创建地址表
CREATE TABLE addresses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(20) NOT NULL DEFAULT 'shipping', -- shipping, billing
street VARCHAR(255) NOT NULL,
city VARCHAR(100) NOT NULL,
state VARCHAR(100),
postal_code VARCHAR(20),
country VARCHAR(100) NOT NULL DEFAULT 'CN',
is_default BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 3. 创建索引(CONCURRENTLY 避免锁表)
CREATE INDEX CONCURRENTLY idx_addresses_user_id ON addresses(user_id);
CREATE INDEX CONCURRENTLY idx_addresses_user_default
ON addresses(user_id) WHERE is_default = TRUE;
CREATE INDEX CONCURRENTLY idx_users_phone_verified
ON users(phone_verified) WHERE phone_verified = TRUE;-- Migration 001: expand_user_table_down.sql(回滚)
DROP INDEX IF EXISTS idx_users_phone_verified;
DROP INDEX IF EXISTS idx_addresses_user_default;
DROP INDEX IF EXISTS idx_addresses_user_id;
DROP TABLE IF EXISTS addresses;
ALTER TABLE users DROP COLUMN IF EXISTS phone_verified;
ALTER TABLE users DROP COLUMN IF EXISTS last_name;
ALTER TABLE users DROP COLUMN IF EXISTS first_name;第二步:Migrate — 数据迁移(迁移 2)
-- Migration 002: migrate_user_data.sql
-- 安全级别:🟡 中风险(数据迁移,需分批执行)
-- 1. 拆分 full_name 到 first_name + last_name(分批处理)
DO $$
DECLARE
batch_size INT := 5000;
total_rows INT;
processed INT := 0;
BEGIN
SELECT count(*) INTO total_rows FROM users WHERE first_name IS NULL;
RAISE NOTICE '需要迁移 % 行数据', total_rows;
LOOP
UPDATE users
SET
first_name = COALESCE(
split_part(full_name, ' ', 1),
full_name
),
last_name = COALESCE(
NULLIF(split_part(full_name, ' ', 2), ''),
''
)
WHERE id IN (
SELECT id FROM users
WHERE first_name IS NULL
ORDER BY id
LIMIT batch_size
);
GET DIAGNOSTICS processed = ROW_COUNT;
EXIT WHEN processed = 0;
RAISE NOTICE '已处理 % 行', processed;
-- 短暂暂停,减少对在线查询的影响
PERFORM pg_sleep(0.1);
END LOOP;
RAISE NOTICE '名字拆分完成';
END $$;
-- 2. 迁移地址数据到 addresses 表(分批处理)
DO $$
DECLARE
batch_size INT := 5000;
processed INT := 0;
BEGIN
LOOP
INSERT INTO addresses (user_id, type, street, city, state, postal_code, country, is_default)
SELECT
u.id,
'shipping',
u.address_street,
u.address_city,
u.address_state,
u.address_postal_code,
COALESCE(u.address_country, 'CN'),
TRUE
FROM users u
WHERE u.address_street IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM addresses a WHERE a.user_id = u.id
)
ORDER BY u.id
LIMIT batch_size;
GET DIAGNOSTICS processed = ROW_COUNT;
EXIT WHEN processed = 0;
RAISE NOTICE '已迁移 % 条地址', processed;
PERFORM pg_sleep(0.1);
END LOOP;
RAISE NOTICE '地址迁移完成';
END $$;
-- 3. 验证数据一致性
DO $$
DECLARE
null_names INT;
missing_addresses INT;
BEGIN
SELECT count(*) INTO null_names
FROM users WHERE first_name IS NULL AND full_name IS NOT NULL;
SELECT count(*) INTO missing_addresses
FROM users u
WHERE u.address_street IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM addresses a WHERE a.user_id = u.id);
IF null_names > 0 THEN
RAISE EXCEPTION '发现 % 个用户名字未迁移!', null_names;
END IF;
IF missing_addresses > 0 THEN
RAISE EXCEPTION '发现 % 个用户地址未迁移!', missing_addresses;
END IF;
RAISE NOTICE '✅ 数据一致性验证通过';
END $$;第三步:部署新代码
// 新代码同时读写新旧字段(过渡期)
class UserService {
async getUser(id: string) {
const user = await prisma.user.findUnique({
where: { id },
include: { addresses: true },
});
return {
...user,
// 优先使用新字段,回退到旧字段
firstName: user.firstName || user.fullName?.split(' ')[0] || '',
lastName: user.lastName || user.fullName?.split(' ')[1] || '',
// 地址优先从 addresses 表读取
address: user.addresses?.[0] || {
street: user.addressStreet,
city: user.addressCity,
},
};
}
async updateUser(id: string, data: UpdateUserDto) {
// 双写:同时更新新旧字段
await prisma.user.update({
where: { id },
data: {
firstName: data.firstName,
lastName: data.lastName,
fullName: `${data.firstName} ${data.lastName}`, // 保持旧字段同步
},
});
}
}第四步:Contract — 清理旧结构(迁移 3,7-14 天后执行)
-- Migration 003: contract_user_table.sql
-- 安全级别:🔴 高风险(删除列,需确认所有代码已迁移)
-- ⚠️ 执行前检查清单:
-- [ ] 所有服务已部署使用新字段的代码
-- [ ] 监控确认无代码访问旧字段
-- [ ] 数据一致性验证通过
-- [ ] 已创建数据库备份
-- 1. 设置新列为 NOT NULL(数据已填充)
ALTER TABLE users ALTER COLUMN first_name SET NOT NULL;
ALTER TABLE users ALTER COLUMN last_name SET NOT NULL;
-- 2. 删除旧列(不可逆!确认后执行)
ALTER TABLE users DROP COLUMN full_name;
ALTER TABLE users DROP COLUMN address_street;
ALTER TABLE users DROP COLUMN address_city;
ALTER TABLE users DROP COLUMN address_state;
ALTER TABLE users DROP COLUMN address_postal_code;
ALTER TABLE users DROP COLUMN address_country;
-- 3. 添加新索引
CREATE INDEX CONCURRENTLY idx_users_last_name ON users(last_name);案例分析
| 阶段 | 风险级别 | 停机时间 | 可回滚 |
|---|---|---|---|
| Expand(迁移 1) | 🟢 低 | 0 | ✅ 完全可逆 |
| Migrate(迁移 2) | 🟡 中 | 0 | ✅ 可逆(删除新数据) |
| 部署新代码 | 🟢 低 | 0 | ✅ 回滚到旧代码 |
| Contract(迁移 3) | 🔴 高 | 0 | ❌ 不可逆(需备份恢复) |
关键决策点:
- 分批处理:500 万行数据的迁移使用 5000 行/批,每批间隔 100ms,总耗时约 17 分钟
- CONCURRENTLY 索引:避免在大表上创建索引时阻塞写入
- 双写过渡期:新代码同时维护新旧字段,确保回滚安全
- 延迟清理:Contract 阶段延迟 7-14 天,留出充足的回滚窗口
- 数据验证:每步迁移后都有一致性验证,确保数据不丢失
避坑指南
❌ 常见错误
-
直接 DROP COLUMN 而不做 Expand-Contract
- 问题:旧版本代码仍在运行时删除列,导致查询报错和服务中断
- 正确做法:先部署不再引用该列的代码,等待所有旧实例下线,再在下一个版本中删除列
-
在大表上使用 CREATE INDEX(非 CONCURRENTLY)
- 问题:PostgreSQL 的 CREATE INDEX 会获取 ShareLock,阻塞所有写入操作;500 万行的表可能锁定数分钟
- 正确做法:PostgreSQL 使用
CREATE INDEX CONCURRENTLY;MySQL 8.0+ 的 INPLACE 算法通常不阻塞 DML
-
新增 NOT NULL 列不提供 DEFAULT 值
- 问题:如果表中已有数据,ALTER TABLE ADD COLUMN … NOT NULL 会失败(PostgreSQL)或需要重写整表(MySQL)
- 正确做法:分三步——先 ADD COLUMN nullable → UPDATE 填充数据 → ALTER COLUMN SET NOT NULL
-
在同一迁移中混合 Schema 变更和数据迁移
- 问题:如果数据迁移失败,Schema 变更也会回滚,导致状态不一致;大数据量迁移会导致长事务
- 正确做法:Schema 变更和数据迁移分开为独立的迁移文件,分别执行和验证
-
生产环境使用 synchronize: true(TypeORM)或 db push(Prisma/Drizzle)
- 问题:自动同步会直接修改生产数据库 Schema,可能导致数据丢失,且没有迁移历史可追溯
- 正确做法:生产环境只使用
migrate deploy(Prisma)、migrate(Drizzle)、migration:run(TypeORM)
-
不测试 Down Migration
- 问题:回滚时才发现 down migration 有 bug,导致回滚失败,陷入进退两难的境地
- 正确做法:在 CI 中自动验证 up → down → up 的可逆性;每次迁移都在测试环境验证回滚
-
AI 生成的迁移不经审查直接应用
- 问题:AI 可能生成包含破坏性操作的迁移(如 DROP COLUMN),或遗漏索引、约束
- 正确做法:所有 AI 生成的迁移必须经过人工审查;使用 Prisma AI Safety Guardrails 或 CI 安全门
-
忽略迁移执行顺序和依赖关系
- 问题:多个开发者同时创建迁移,合并后执行顺序错误,导致外键引用不存在的表
- 正确做法:合并前检查迁移依赖链;使用
alembic check、drizzle-kit check等工具验证一致性
-
大表数据迁移不分批处理
- 问题:一次性 UPDATE 500 万行会产生巨大的 WAL 日志、长时间锁定行、可能导致 OOM
- 正确做法:使用分批处理(batch_size 5000-10000),每批之间加入短暂暂停(pg_sleep)
-
迁移文件中硬编码环境特定的值
- 问题:开发环境的数据库名、用户名等硬编码在迁移文件中,部署到生产时出错
- 正确做法:迁移文件只包含 Schema 变更逻辑,环境配置通过环境变量或配置文件注入
✅ 最佳实践
- 迁移文件是不可变的历史记录:一旦迁移被应用到任何环境,就不应该修改它;需要修正时创建新的迁移
- 每个迁移只做一件事:单一职责原则同样适用于迁移文件,便于审查、测试和回滚
- 先迁移数据库,再部署代码:确保新代码部署时数据库已经准备好新结构
- 使用 Expand-Contract 模式处理所有破坏性变更:这是零停机迁移的黄金法则
- 在 CI 中自动化迁移安全检查:使用 GitHub Actions + 破坏性操作检测 + 回滚验证
- 保持迁移文件的可读性:添加注释说明变更原因、风险评估和回滚方案
- 定期清理迁移历史:对于长期项目,定期将旧迁移合并为基线迁移(squash migrations)
- 监控迁移执行时间:在生产环境记录每次迁移的执行时间,建立基线,异常时告警
- 使用 Kiro Hooks 自动审查:配置迁移文件变更触发 AI 安全审查,减少人工遗漏
- 建立迁移审查清单:团队共享的迁移审查清单,确保每次迁移都经过标准化检查
相关资源与延伸阅读
- Prisma Migrate 官方文档 :Prisma 迁移的完整指南,包含开发和生产环境的最佳实践
- Drizzle Kit 官方文档 :Drizzle 迁移工具的完整命令参考和配置指南
- Alembic 官方教程 :Alembic 从入门到高级用法的完整教程,包含 autogenerate 详解
- Diesel 迁移指南 :Diesel ORM 的迁移系统入门指南,覆盖 SQL-first 迁移工作流
- Django Migrations 官方文档 :Django 迁移系统的完整参考,包含数据迁移和高级用法
- Atlas — Modern Database CI/CD :Atlas 声明式迁移工具的 CI/CD 集成指南
- Bytebase SQL Review CI :Bytebase 的自动化 SQL 审查 CI 集成文档
- Safe Django Migrations — PostHog :PostHog 团队总结的安全 Django 迁移实践,包含两阶段删除规则
- How to Perform Zero-Downtime Database Migrations :零停机数据库迁移的完整方法论,覆盖 Expand-Contract 模式
- Flyway 官方文档 :Flyway 数据库版本管理工具的完整文档,适用于 Java 生态和企业级项目
参考来源
- Prisma ORM 6.15.0 — AI Safety Guardrails for Destructive Commands (2025-06)
- Prisma ORM 6.6.0 — ESM Support, D1 Migrations & Prisma MCP Server (2025-04)
- Prisma vs. Drizzle: Let AI Write Your Database Migrations (2026-02)
- Drizzle Kit — CLI Migrator Tool for Drizzle ORM (2025-06)
- Drizzle ORM Migrations Documentation (2025)
- Fail-Proof DB Migrations: FastAPI × SQLAlchemy × Alembic (2025-10)
- No-Fail Guide: Getting Started with Database Migrations (FastAPI × SQLAlchemy × Alembic) (2025-08)
- Auto Generating Migrations — Alembic 1.18 Documentation (2025)
- TypeORM Migrations in NestJS with SQLite: A Complete Guide (2025-03)
- TypeORM V3 CLI Path Issues Solved: Master Migrations with DataSource (2025-06)
- How to Connect Rust Applications to PostgreSQL with Diesel (2026-01)
- Diesel Migration Redo Guide (2025-06)
- A Comprehensive Guide to Django Migrations in 2026 (2026-01)
- Safe Django migrations without server errors (2025-06)
- Safe Django Migrations — PostHog Handbook (2025)
- How to Perform Zero-Downtime Database Migrations (2026-02)
- How to Design Database Migrations That Never Need Rollback (2026-02)
- How to Build a CI/CD Pipeline for Database Schema Migration — Bytebase (2025-06)
- How to Run Database Migrations with GitHub Actions (2026-01)
- 8 AI Coding Agents That Actually Accelerate Database Schema Migrations (2025-06)
- Modern Database CI/CD with Atlas (2025)
- A Gentle Introduction to Database Migrations in Prisma with Visuals (2025-04)
- Prisma ORM Production Guide for Next.js (2025-06)
📖 返回 总览与导航 | 上一节:29b-AI辅助Schema设计 | 下一节:29d-ORM与查询优化