6月5日 13:56

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 自动对比生成迁移:

bash
npx typeorm migration:generate src/migration/AddUserEmail -d src/data-source.ts

TypeORM 会对比实体定义和当前数据库结构,生成差量的迁移文件。这是最安全的方式——不会漏字段、不会写错类型。

前提:数据库必须和当前代码的实体一致(上次迁移已执行)。如果数据库和实体不同步,生成会报错。

方式二:手动创建

bash
npx typeorm migration:create src/migration/AddUserEmail

创建空模板,自己写 SQL:

typescript
import { 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();

或在启动时自动跑:

typescript
async function bootstrap() { const app = await NestFactory.create(AppModule); // 启动时自动执行迁移 const dataSource = app.get(DataSource); await dataSource.runMigrations(); await app.listen(3000); }

注意:生产环境自动跑迁移有风险——如果迁移有 bug,服务启动就失败。更安全的做法是在部署流程里单独跑迁移,确认成功后再部署新代码。

常见迁移场景

加列

typescript
public 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 会导致已有行插入失败。

删列

typescript
public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropColumn('user', 'deprecatedField'); }

删列不可逆——数据丢了就没了。确保没有代码引用这个列后再删。建议先在代码里移除对列的引用,部署一版,确认没有报错,再加迁移删列。

改列类型

typescript
public 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) 会截断数据。改之前检查现有数据是否都在新范围内。

重命名列

typescript
public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.renameColumn('user', 'name', 'fullName'); }

renameColumn 比 "删旧列+加新列" 安全——它用 ALTER TABLE RENAME COLUMN,数据不丢失。

数据迁移

纯 schema 迁移只改表结构,不改数据。数据迁移需要在 up() 里写 SQL:

typescript
public 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 `);

迁移最佳实践

  1. 每个迁移只做一件事——加列是一个迁移,改类型是另一个。出错时可以精确回滚
  2. 先加列后删列——加列不影响现有代码,删列必须先改代码再跑迁移
  3. 生产迁移先在 staging 测试——同样的迁移在测试环境跑一遍确认没有报错
  4. 永远不要手动修改 _migration——TypeORM 靠它判断哪些迁移已执行,手动改会导致迁移混乱
  5. 迁移文件提交到 git——团队成员 pull 后跑 migration:run 就能同步数据库结构
  6. down() 必须正确实现——回滚时 down() 是唯一的恢复手段
  7. 部署流程里迁移先行——先跑迁移再部署新代码,避免新代码期望新列但列还不存在
标签:TypeORM