第 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 }}部署前自动跑,失败则拒绝部署。
八、生产部署策略
- 先发迁移(只加列、不删数据)
- 等旧代码兼容(旧字段可空)
- 发新代码(读新字段)
- 观察几天
- 清理迁移(删旧字段)
⚠️ 坑 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 | 状态 |
动手练习
- 生成迁移:从 User 实体生成 InitSchema,跑 migration:run
- 手写迁移:加
email字段,可空,跑 up 再跑 revert - 数据回填:给 email 加默认值后再改成 NOT NULL
下一章:第 17 章:JWT 认证 →