Skip to content
第 16 章 后端 ⏱ 12 分钟阅读

第 16 章:迁移 ​

学习目标 ​

  • 理解 Migration 的价值
  • 生成 Migration 文件
  • 跑迁移、回滚、CI 集成
  • 避开 3 个迁移坑

一、为什么需要 Migration ​

synchronize: true 自动建表,开发够用,生产禁用。Migration 提供:

  • 版本化的表结构变更
  • 团队共享 DDL
  • 可回滚
  • CI/CD 自动执行

二、配置(关闭 synchronize) ​

typescript
TypeOrmModule.forRoot({
  // ...
  synchronize: false,                // 必关
  migrations: ['src/migrations/*.ts'],
  migrationsTableName: 'migrations',
});

三、生成第一个迁移 ​

bash
# 方式 1:从实体生成
pnpm typeorm migration:generate src/migrations/InitSchema -d src/data-source.ts

# 方式 2:手写空迁移
pnpm typeorm migration:create src/migrations/AddUserEmail

需要 data-source.ts:

typescript
// src/data-source.ts
import 'reflect-metadata';
import { DataSource } from 'typeorm';
import { User } from './user/user.entity';

export default new DataSource({
  type: 'postgres',
  host: process.env.DB_HOST,
  port: 5432,
  username: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_NAME,
  entities: [User],
  migrations: ['src/migrations/*.ts'],
});

生成的 InitSchema.ts:

typescript
import { MigrationInterface, QueryRunner } from 'typeorm';

export class InitSchema1716000000000 implements MigrationInterface {
  name = 'InitSchema1716000000000';

  public async up(qr: QueryRunner): Promise<void> {
    await qr.query(`CREATE TABLE "users" (
      "id" SERIAL PRIMARY KEY,
      "username" VARCHAR(50) NOT NULL UNIQUE,
      "password" VARCHAR NOT NULL,
      "active" BOOLEAN DEFAULT true,
      "createdAt" TIMESTAMP NOT NULL DEFAULT now()
    )`);
  }

  public async down(qr: QueryRunner): Promise<void> {
    await qr.query(`DROP TABLE "users"`);
  }
}

四、跑迁移 ​

bash
# 执行所有未跑的迁移
pnpm typeorm migration:run -d src/data-source.ts

# 回滚最后一次
pnpm typeorm migration:revert -d src/data-source.ts

# 查看状态
pnpm typeorm migration:show -d src/data-source.ts

⚠️ 坑 1:改了实体但没生成迁移 → 上线后表结构没变 → 跑 migration:generate。

五、手写迁移(精细控制) ​

typescript
// src/migrations/AddUserEmail.ts
export class AddUserEmail1717000000000 implements MigrationInterface {
  public async up(qr: QueryRunner) {
    await qr.addColumn('users', new TableColumn({
      name: 'email',
      type: 'varchar',
      isNullable: true,
    }));
    await qr.createIndex('users', new TableIndex({
      name: 'IDX_USER_EMAIL',
      columnNames: ['email'],
    }));
  }

  public async down(qr: QueryRunner) {
    await qr.dropIndex('users', 'IDX_USER_EMAIL');
    await qr.dropColumn('users', 'email');
  }
}

六、数据迁移(回填) ​

typescript
public async up(qr: QueryRunner) {
  await qr.addColumn('users', new TableColumn({ name: 'email', type: 'varchar', isNullable: true }));
  // 回填
  await qr.query(`UPDATE "users" SET "email" = username || '@example.com'`);
  await qr.changeColumn('users', 'email', new TableColumn({ name: 'email', type: 'varchar', isNullable: false }));
}

注意:加列要可空 → 回填 → 改非空,避免大表 UPDATE 锁表。

⚠️ 坑 2:大表加非空字段不带默认值 → 全表锁住,生产事故。

七、CI/CD 集成 ​

yaml
# .github/workflows/deploy.yml
- name: Run migrations
  run: pnpm typeorm migration:run -d src/data-source.ts
  env:
    DB_HOST: ${{ secrets.DB_HOST }}
    DB_USER: ${{ secrets.DB_USER }}
    DB_PASS: ${{ secrets.DB_PASS }}

部署前自动跑,失败则拒绝部署。

八、生产部署策略 ​

  1. 先发迁移(只加列、不删数据)
  2. 等旧代码兼容(旧字段可空)
  3. 发新代码(读新字段)
  4. 观察几天
  5. 清理迁移(删旧字段)

⚠️ 坑 3:一次性"删列 + 改代码 + 部署" → 旧实例读不到字段直接报错。

九、运行时跑迁移(谨慎) ​

typescript
import { DataSource } from 'typeorm';

@Injectable()
export class MigrationService {
  constructor(private readonly ds: DataSource) {}

  async run() {
    await this.ds.runMigrations({ transaction: 'each' });     // 每条独立事务
  }
}

建议还是放到 CI/CD。

十、本章小结 ​

命令作用
migration:generate从实体差异生成
migration:create手写空迁移
migration:run执行
migration:revert回滚
migration:show状态

动手练习 ​

  1. 生成迁移:从 User 实体生成 InitSchema,跑 migration:run
  2. 手写迁移:加 email 字段,可空,跑 up 再跑 revert
  3. 数据回填:给 email 加默认值后再改成 NOT NULL

下一章:第 17 章:JWT 认证 →

本站基于 VitePress 构建 · 由 StackHub 团队维护