TypeORM迁移完整指南:自动生成、数据迁移和生产部署策略
synchronize: true 在开发时很方便——改实体自动同步表结构。但生产环境这么做会丢数据:删字段时直接 DROP COLUMN,重命名字段被当作"删旧的加新的"。迁移(Migration)是生产环境管理数据库结构变更的唯一正确方式。这篇文章把迁移的完整流程和常见坑都讲清楚。
迁移的工作原理
迁移就是一个类,有 up() 和 down() 两个方法——up() 执行变更,down() 回滚变更。TypeORM 按顺序执行迁移文件,并在数据库里记录哪些已经跑过。
shell数据表 _migration: ┌──────────────────────────────┬────────────┐ │ id │ timestamp │ ├──────────────────────────────┼────────────┤ │ InitSchema1700000000000 │ 2024-01-01 │ │ AddUserEmail1700000000001 │ 2024-01-02 │ └──────────────────────────────┴────────────┘
每次 migration:run,TypeORM 对比已执行的迁移和待执行的迁移文件,只跑新的。
创建迁移
方式一:自动生成(推荐)
修改实体后,让 TypeORM 自动对比生成迁移:
bashnpx typeorm migration:generate src/migration/AddUserEmail -d src/data-source.ts
TypeORM 会对比实体定义和当前数据库结构,生成差量的迁移文件。这是最安全的方式——不会漏字段、不会写错类型。
前提:数据库必须和当前代码的实体一致(上次迁移已执行)。如果数据库和实体不同步,生成会报错。
方式二:手动创建
bashnpx typeorm migration:create src/migration/AddUserEmail
创建空模板,自己写 SQL:
typescriptimport { MigrationInterface, QueryRunner } from 'typeorm'; export class AddUserEmail1700000000001 implements MigrationInterface { name = 'AddUserEmail1700000000001'; public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.addColumn( 'user', new TableColumn({ name: 'email', type: 'varchar', length: '255', isUnique: true, }), ); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropColumn('user', 'email'); } }
手动迁移用于自动生成搞不定的场景:数据迁移(把数据从一列搬到另一列)、复杂的 schema 重构。
执行迁移
bash# 执行所有未执行的迁移 npx typeorm migration:run -d src/data-source.ts # 回滚最后一次迁移 npx typeorm migration:revert -d src/data-source.ts # 查看迁移状态 npx typeorm migration:show -d src/data-source.ts
migration:show 输出:
shell[X] InitSchema1700000000000 # 已执行 [X] AddUserEmail1700000000001 # 已执行 [ ] AddPostTags1700000000002 # 未执行
在 NestJS 里执行
typescript// main.ts 或专门的迁移脚本 import { DataSource } from 'typeorm'; import { AppDataSource } from './data-source'; async function runMigrations() { await AppDataSource.initialize(); await AppDataSource.runMigrations(); await AppDataSource.destroy(); } runMigrations();
或在启动时自动跑:
typescriptasync function bootstrap() { const app = await NestFactory.create(AppModule); // 启动时自动执行迁移 const dataSource = app.get(DataSource); await dataSource.runMigrations(); await app.listen(3000); }
注意:生产环境自动跑迁移有风险——如果迁移有 bug,服务启动就失败。更安全的做法是在部署流程里单独跑迁移,确认成功后再部署新代码。
常见迁移场景
加列
typescriptpublic async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.addColumn('user', new TableColumn({ name: 'email', type: 'varchar', isNullable: true, // 先允许 NULL,后续填充数据后再设 NOT NULL })); }
安全做法:新列先 isNullable: true,应用层填充数据后,再用另一个迁移改为 NOT NULL。直接 NOT NULL 会导致已有行插入失败。
删列
typescriptpublic async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropColumn('user', 'deprecatedField'); }
删列不可逆——数据丢了就没了。确保没有代码引用这个列后再删。建议先在代码里移除对列的引用,部署一版,确认没有报错,再加迁移删列。
改列类型
typescriptpublic async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.changeColumn('user', 'age', new TableColumn({ name: 'age', type: 'int', // 从 smallint 改为 int isNullable: false, })); }
类型变更可能导致数据丢失——varchar(255) 改 varchar(50) 会截断数据。改之前检查现有数据是否都在新范围内。
重命名列
typescriptpublic async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.renameColumn('user', 'name', 'fullName'); }
renameColumn 比 "删旧列+加新列" 安全——它用 ALTER TABLE RENAME COLUMN,数据不丢失。
数据迁移
纯 schema 迁移只改表结构,不改数据。数据迁移需要在 up() 里写 SQL:
typescriptpublic async up(queryRunner: QueryRunner): Promise<void> { // 把 name 拆成 firstName 和 lastName await queryRunner.addColumn('user', new TableColumn({ name: 'firstName', type: 'varchar', isNullable: true })); await queryRunner.addColumn('user', new TableColumn({ name: 'lastName', type: 'varchar', isNullable: true })); // 数据迁移 await queryRunner.query(` UPDATE "user" SET "firstName" = split_part("name", ' ', 1), "lastName" = split_part("name", ' ', 2) `); // 数据填充完后删除旧列 await queryRunner.dropColumn('user', 'name'); }
数据迁移要注意性能——百万级表的 UPDATE 可能跑几十分钟。大表迁移分批执行:
typescript// 分批更新,每批 1000 行 await queryRunner.query(` UPDATE "user" SET "firstName" = split_part("name", ' ', 1) WHERE "firstName" IS NULL LIMIT 1000 `);
迁移最佳实践
- 每个迁移只做一件事——加列是一个迁移,改类型是另一个。出错时可以精确回滚
- 先加列后删列——加列不影响现有代码,删列必须先改代码再跑迁移
- 生产迁移先在 staging 测试——同样的迁移在测试环境跑一遍确认没有报错
- 永远不要手动修改
_migration表——TypeORM 靠它判断哪些迁移已执行,手动改会导致迁移混乱 - 迁移文件提交到 git——团队成员 pull 后跑
migration:run就能同步数据库结构 down()必须正确实现——回滚时down()是唯一的恢复手段- 部署流程里迁移先行——先跑迁移再部署新代码,避免新代码期望新列但列还不存在