Database Migration
掌握跨 ORM(Sequelize、TypeORM、Prisma)的数据库模式和数据迁移,包括回滚策略和零停机部署。
何时不使用此技能
- 任务与数据库迁移无关
- 需要此范围之外的其他领域或工具
使用说明
- 明确目标、约束和所需输入。
- 应用相关最佳实践并验证结果。
- 提供可执行的步骤和验证方法。
- 如需详细示例,请打开
resources/implementation-playbook.md。
何时使用此技能
- 在不同 ORM 之间迁移
- 执行模式转换
- 在数据库之间移动数据
- 实现回滚流程
- 零停机部署
- 数据库版本升级
- 数据模型重构
ORM 迁移
Sequelize 迁移
// migrations/20231201-create-users.js
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('users', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
email: {
type: Sequelize.STRING,
unique: true,
allowNull: false
},
createdAt: Sequelize.DATE,
updatedAt: Sequelize.DATE
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('users');
}
};
// 运行: npx sequelize-cli db:migrate
// 回滚: npx sequelize-cli db:migrate:undo
TypeORM 迁移
// migrations/1701234567-CreateUsers.ts
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
export class CreateUsers1701234567 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'users',
columns: [
{
name: 'id',
type: 'int',
isPrimary: true,
isGenerated: true,
generationStrategy: 'increment'
},
{
name: 'email',
type: 'varchar',
isUnique: true
},
{
name: 'created_at',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP'
}
]
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('users');
}
}
// 运行: npm run typeorm migration:run
// 回滚: npm run typeorm migration:revert
Prisma 迁移
// schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
createdAt DateTime @default(now())
}
// 生成迁移: npx prisma migrate dev --name create_users
// 应用迁移: npx prisma migrate deploy
模式转换
添加带默认值的列
// 安全迁移:添加带默认值的列
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('users', 'status', {
type: Sequelize.STRING,
defaultValue: 'active',
allowNull: false
});
},
down: async (queryInterface) => {
await queryInterface.removeColumn('users', 'status');
}
};
重命名列(零停机)
// 步骤 1:添加新列
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('users', 'full_name', {
type: Sequelize.STRING
});
// 从旧列复制数据
await queryInterface.sequelize.query(
'UPDATE users SET full_name = name'
);
},
down: async (queryInterface) => {
await queryInterface.removeColumn('users', 'full_name');
}
};
// 步骤 2:更新应用程序以使用新列
// 步骤 3:删除旧列
module.exports = {
up: async (queryInterface) => {
await queryInterface.removeColumn('users', 'name');
},
down: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('users', 'name', {
type: Sequelize.STRING
});
}
};
更改列类型
module.exports = {
up: async (queryInterface, Sequelize) => {
// 对于大表,使用多步骤方法
// 1. 添加新列
await queryInterface.addColumn('users', 'age_new', {
type: Sequelize.INTEGER
});
// 2. 复制并转换数据
await queryInterface.sequelize.query(`
UPDATE users
SET age_new = CAST(age AS INTEGER)
WHERE age IS NOT NULL
`);
// 3. 删除旧列
await queryInterface.removeColumn('users', 'age');
// 4. 重命名新列
await queryInterface.renameColumn('users', 'age_new', 'age');
},
down: async (queryInterface, Sequelize) => {
await queryInterface.changeColumn('users', 'age', {
type: Sequelize.STRING
});
}
};
数据转换
复杂数据迁移
module.exports = {
up: async (queryInterface, Sequelize) => {
// 获取所有记录
const [users] = await queryInterface.sequelize.query(
'SELECT id, address_string FROM users'
);
// 转换每条记录
for (const user of users) {
const addressParts = user.address_string.split(',');
await queryInterface.sequelize.query(
`UPDATE users
SET street = :street,
city = :city,
state = :state
WHERE id = :id`,
{
replacements: {
id: user.id,
street: addressParts[0]?.trim(),
city: addressParts[1]?.trim(),
state: addressParts[2]?.trim()
}
}
);
}
// 删除旧列
await queryInterface.removeColumn('users', 'address_string');
},
down: async (queryInterface, Sequelize) => {
// 重建原始列
await queryInterface.addColumn('users', 'address_string', {
type: Sequelize.STRING
});
await queryInterface.sequelize.query(`
UPDATE users
SET address_string = CONCAT(street, ', ', city, ', ', state)
`);
await queryInterface.removeColumn('users', 'street');
await queryInterface.removeColumn('users', 'city');
await queryInterface.removeColumn('users', 'state');
}
};
回滚策略
基于事务的迁移
module.exports = {
up: async (queryInterface, Sequelize) => {
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.addColumn(
'users',
'verified',
{ type: Sequelize.BOOLEAN, defaultValue: false },
{ transaction }
);
await queryInterface.sequelize.query(
'UPDATE users SET verified = true WHERE email_verified_at IS NOT NULL',
{ transaction }
);
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
},
down: async (queryInterface) => {
await queryInterface.removeColumn('users', 'verified');
}
};
基于检查点的回滚
module.exports = {
up: async (queryInterface, Sequelize) => {
// 创建备份表
await queryInterface.sequelize.query(
'CREATE TABLE users_backup AS SELECT * FROM users'
);
try {
// 执行迁移
await queryInterface.addColumn('users', 'new_field', {
type: Sequelize.STRING
});
// 验证迁移
const [result] = await queryInterface.sequelize.query(
"SELECT COUNT(*) as count FROM users WHERE new_field IS NULL"
);
if (result[0].count > 0) {
throw new Error('Migration verification failed');
}
// 删除备份
await queryInterface.dropTable('users_backup');
} catch (error) {
// 从备份恢复
await queryInterface.sequelize.query('DROP TABLE users');
await queryInterface.sequelize.query(
'CREATE TABLE users AS SELECT * FROM users_backup'
);
await queryInterface.dropTable('users_backup');
throw error;
}
}
};
零停机迁移
蓝绿部署策略
// 阶段 1:使变更向后兼容
module.exports = {
up: async (queryInterface, Sequelize) => {
// 添加新列(旧代码和新代码都可以工作)
await queryInterface.addColumn('users', 'email_new', {
type: Sequelize.STRING
});
}
};
// 阶段 2:部署同时写入两列的代码
// 阶段 3:回填数据
module.exports = {
up: async (queryInterface) => {
await queryInterface.sequelize.query(`
UPDATE users
SET email_new = email
WHERE email_new IS NULL
`);
}
};
// 阶段 4:部署从新列读取的代码
// 阶段 5:删除旧列
module.exports = {
up: async (queryInterface) => {
await queryInterface.removeColumn('users', 'email');
}
};
跨数据库迁移
PostgreSQL 到 MySQL
// 处理差异
module.exports = {
up: async (queryInterface, Sequelize) => {
const dialectName = queryInterface.sequelize.getDialect();
if (dialectName === 'mysql') {
await queryInterface.createTable('users', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
data: {
type: Sequelize.JSON // MySQL JSON 类型
}
});
} else if (dialectName === 'postgres') {
await queryInterface.createTable('users', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
data: {
type: Sequelize.JSONB // PostgreSQL JSONB 类型
}
});
}
}
};
资源
- references/orm-switching.md: ORM 迁移指南
- references/schema-migration.md: 模式转换模式
- references/data-transformation.md: 数据迁移脚本
- references/rollback-strategies.md: 回滚流程
- assets/schema-migration-template.sql: SQL 迁移模板
- assets/data-migration-script.py: 数据迁移工具
- scripts/test-migration.sh: 迁移测试脚本
最佳实践
- 始终提供回滚:每个 up() 都需要对应的 down()
- 测试迁移:先在预发环境测试
- 使用事务:尽可能使用原子迁移
- 先备份:迁移前始终备份
- 小步变更:分解为小的增量步骤
- 监控:部署期间注意观察错误
- 文档化:解释原因和方法
- 幂等性:迁移应该可重复运行
常见陷阱
- 未测试回滚流程
- 在没有停机策略的情况下进行破坏性变更
- 忘记处理 NULL 值
- 未考虑索引性能
- 忽略外键约束
- 一次迁移过多数据
局限性
- 仅当任务明确符合上述描述的范围时使用此技能。
- 不要将输出作为环境特定验证、测试或专家审查的替代品。
- 如果缺少所需输入、权限、安全边界或成功标准,请停止并请求澄清。