迁移系统是 TypeORM 中用于管理数据库结构变更的重要工具,它允许开发者以版本化的方式追踪和应用数据库结构的变化,确保团队协作时数据库结构的一致性。
迁移基础概念
什么是迁移
迁移是数据库结构变更的脚本,用于:
- 创建或删除表
- 添加或删除列
- 修改列类型
- 创建或删除索引
- 添加或删除外键约束
每个迁移都有唯一的版本号和时间戳,确保迁移可以按顺序执行。
迁移文件结构
typescriptimport { MigrationInterface, QueryRunner, Table } from 'typeorm'; export class CreateUserTable1234567890123 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { // 执行迁移:创建表、添加列等 await queryRunner.createTable( new Table({ name: 'user', columns: [ { name: 'id', type: 'int', isPrimary: true, isGenerated: true, generationStrategy: 'increment', }, { name: 'name', type: 'varchar', }, { name: 'email', type: 'varchar', isUnique: true, }, { name: 'createdAt', type: 'timestamp', default: 'CURRENT_TIMESTAMP', }, ], }), true ); } public async down(queryRunner: QueryRunner): Promise<void> { // 回滚迁移:删除表、移除列等 await queryRunner.dropTable('user'); } }
创建迁移
使用 CLI 创建迁移
bash# 创建新迁移 npm run typeorm migration:generate -- -n CreateUserTable # 或者使用 npx npx typeorm migration:generate -n CreateUserTable # 创建空迁移 npm run typeorm migration:create -- -n CreateUserTable
配置 DataSource
typescriptimport { DataSource } from 'typeorm'; export const AppDataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: ['src/entity/**/*.ts'], migrations: ['src/migration/**/*.ts'], subscribers: ['src/subscriber/**/*.ts'], synchronize: false, // 生产环境必须设为 false logging: true, });
运行迁移
使用 CLI 运行迁移
bash# 运行所有待执行的迁移 npm run typeorm migration:run # 回滚最后一次迁移 npm run typeorm migration:revert # 显示迁移状态 npm run typeorm migration:show # 清空数据库(慎用) npm run typeorm schema:drop
在代码中运行迁移
typescriptimport { DataSource } from 'typeorm'; async function runMigrations() { const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: ['src/entity/**/*.ts'], migrations: ['src/migration/**/*.ts'], }); await dataSource.initialize(); // 运行所有待执行的迁移 await dataSource.runMigrations(); // 回滚最后一次迁移 // await dataSource.undoLastMigration(); await dataSource.destroy(); } runMigrations().catch(console.error);
迁移操作示例
创建表
typescriptpublic async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.createTable( new Table({ name: 'user', columns: [ { name: 'id', type: 'int', isPrimary: true, isGenerated: true, generationStrategy: 'increment', }, { name: 'name', type: 'varchar', length: '100', }, { name: 'email', type: 'varchar', length: '255', isUnique: true, }, { name: 'age', type: 'int', nullable: true, }, { name: 'isActive', type: 'boolean', default: true, }, { name: 'createdAt', type: 'timestamp', default: 'CURRENT_TIMESTAMP', }, { name: 'updatedAt', type: 'timestamp', default: 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP', }, ], indices: [ { name: 'IDX_USER_EMAIL', columnNames: ['email'], }, ], }), true ); }
添加列
typescriptpublic async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.addColumn( 'user', new TableColumn({ name: 'avatar', type: 'varchar', length: '255', isNullable: true, }) ); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropColumn('user', 'avatar'); }
修改列
typescriptpublic async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.changeColumn( 'user', 'name', new TableColumn({ name: 'name', type: 'varchar', length: '200', // 修改长度 }) ); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.changeColumn( 'user', 'name', new TableColumn({ name: 'name', type: 'varchar', length: '100', }) ); }
创建索引
typescriptpublic async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.createIndex( 'user', new TableIndex({ name: 'IDX_USER_EMAIL', columnNames: ['email'], isUnique: true, }) ); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropIndex('user', 'IDX_USER_EMAIL'); }
添加外键
typescriptpublic async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.createForeignKey( 'post', new TableForeignKey({ columnNames: ['authorId'], referencedColumnNames: ['id'], referencedTableName: 'user', onDelete: 'CASCADE', }) ); } public async down(queryRunner: QueryRunner): Promise<void> { const table = await queryRunner.getTable('post'); const foreignKey = table.foreignKeys.find( fk => fk.columnNames.indexOf('authorId') !== -1 ); await queryRunner.dropForeignKey('post', foreignKey); }
执行原生 SQL
typescriptpublic async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(` CREATE TRIGGER update_user_timestamp BEFORE UPDATE ON user FOR EACH ROW SET NEW.updatedAt = CURRENT_TIMESTAMP `); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(`DROP TRIGGER update_user_timestamp`); }
数据迁移
迁移现有数据
typescriptpublic async up(queryRunner: QueryRunner): Promise<void> { // 添加新列 await queryRunner.addColumn( 'user', new TableColumn({ name: 'fullName', type: 'varchar', length: '200', isNullable: true, }) ); // 迁移数据 await queryRunner.query(` UPDATE user SET fullName = CONCAT(firstName, ' ', lastName) `); // 删除旧列 await queryRunner.dropColumn('user', 'firstName'); await queryRunner.dropColumn('user', 'lastName'); }
批量插入数据
typescriptpublic async up(queryRunner: QueryRunner): Promise<void> { const users = [ { name: 'John', email: 'john@example.com' }, { name: 'Jane', email: 'jane@example.com' }, ]; for (const user of users) { await queryRunner.query( `INSERT INTO user (name, email) VALUES (?, ?)`, [user.name, user.email] ); } }
迁移最佳实践
1. 版本控制
typescript// 迁移文件命名格式: {timestamp}-{name}.ts // 例如: 1234567890123-CreateUserTable.ts export class CreateUserTable1234567890123 implements MigrationInterface { // 迁移内容 }
2. 可逆性
确保每个迁移都可以完全回滚:
typescriptexport class AddUserAvatar1234567890123 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.addColumn('user', new TableColumn({ name: 'avatar', type: 'varchar', isNullable: true, })); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropColumn('user', 'avatar'); } }
3. 事务支持
typescriptpublic async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.startTransaction(); try { await queryRunner.createTable(/* ... */); await queryRunner.addColumn(/* ... */); await queryRunner.commitTransaction(); } catch (err) { await queryRunner.rollbackTransaction(); throw err; } }
4. 环境区分
typescriptexport class AddProductionIndex1234567890123 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { // 只在生产环境执行 if (process.env.NODE_ENV === 'production') { await queryRunner.createIndex(/* ... */); } } public async down(queryRunner: QueryRunner): Promise<void> { if (process.env.NODE_ENV === 'production') { await queryRunner.dropIndex(/* ... */); } } }
常见问题
1. 迁移冲突
当多个开发者同时创建迁移时可能出现冲突:
bash# 解决方法:重新生成迁移 npm run typeorm migration:generate -- -n FixMigrationConflict
2. 数据丢失风险
在修改列类型或删除列前备份数据:
typescriptpublic async up(queryRunner: QueryRunner): Promise<void> { // 备份数据 await queryRunner.query(`CREATE TABLE user_backup AS SELECT * FROM user`); // 执行迁移 await queryRunner.changeColumn(/* ... */); }
3. 性能问题
对于大数据表的迁移,考虑分批处理:
typescriptpublic async up(queryRunner: QueryRunner): Promise<void> { const batchSize = 1000; let offset = 0; while (true) { const users = await queryRunner.query( `SELECT id FROM user LIMIT ${batchSize} OFFSET ${offset}` ); if (users.length === 0) break; for (const user of users) { await queryRunner.query(`UPDATE user SET ... WHERE id = ?`, [user.id]); } offset += batchSize; } }
生产环境建议
- 禁用 synchronize: 生产环境必须设置
synchronize: false - 备份策略: 执行迁移前备份数据库
- 测试环境: 先在测试环境验证迁移
- 回滚计划: 准备好回滚方案
- 监控日志: 监控迁移执行日志
- 分步执行: 对于大型迁移,分步骤执行
TypeORM 的迁移系统提供了强大而灵活的数据库结构管理能力,掌握迁移系统的使用对于维护大型应用的数据库结构至关重要。