workflow.md 9.5 KB


name: create-db-migration

description: TypeORM 数据库迁移工作流 - 双 Data Source 架构下的标准化迁移创建流程

TypeORM 数据库迁移工作流

目标: 在双 Data Source 架构下,标准化 TypeORM 数据库迁移的创建和执行流程。

你的角色: 数据库迁移专家,熟悉 TypeORM CLI、双 Data Source 架构、PostgreSQL 数据库操作。


工作流架构

这是一个单流程、检查清单驱动的工作流:

  • 每个步骤必须按顺序完成
  • 每个步骤都有明确的验证标准
  • 失败时停止并解决问题后再继续

背景:双 Data Source 架构

为什么需要两个 Data Source?

TypeORM CLI 的技术限制:

Data Source 文件 用途 实体加载方式
API Data Source packages/server/src/data-source.ts 运行时 API 使用 类数组 Object.values(entities)
CLI Data Source packages/server/src/data-source-cli.ts TypeORM CLI 迁移 Glob 模式 "../**/*.entity.ts"

关键约束:

  • TypeORM CLI 不支持直接传递类数组
  • CLI 必须使用 Glob 模式加载实体
  • 两个文件都有存在必要,不可合并

好消息:

  • CLI 的 Glob 已配置覆盖所有模块路径
  • 新增实体无需修改任何配置
  • 迁移文件自动被 CLI 发现

执行流程

步骤 1:前置检查

检查实体文件是否已创建:

# 确认实体文件存在
ls allin-packages/[module-name]/src/entities/[entity-name].entity.ts

检查实体是否在 Glob 覆盖范围内:

查看 packages/server/src/data-source-cli.ts 确认实体路径已被包含:

entities: [
  "../allin-packages/[your-module]/src/entities/*.ts",  // 确认你的模块在这里
  // ...
]

检查表是否已存在于数据库:

psql -h 127.0.0.1 -U postgres -c "\dt [table_name]"

如果表已存在,停止工作流 - 考虑是否需要修改表而不是创建新表。


步骤 2:生成迁移文件

进入 server 目录:

cd packages/server

运行 TypeORM 迁移生成命令:

pnpm migration:generate -- -n [MigrationName]

迁移命名规范:

  • 使用 PascalCase
  • 描述性名称,如 CreateDisabledPersonPhoneTable
  • 如涉及数据迁移,添加 AndMigrateData 后缀

预期输出:

Migration /path/to/packages/server/migrations/[timestamp]-[MigrationName].ts has been generated successfully.

步骤 3:审查生成的迁移文件

打开生成的迁移文件:

# 迁移文件位于
packages/migrations/[timestamp]-[MigrationName].ts

检查清单:

  • up() 方法包含正确的 CREATE TABLEALTER TABLE 语句
  • down() 方法包含正确的回滚逻辑
  • 列类型、约束、索引与实体定义一致
  • 外键关系正确设置
  • CASCADE 删除策略符合预期

常见问题修复:

问题 原因 解决方案
迁移为空 实体与数据库表已同步 检查是否真的需要迁移
缺少索引 实体 @Index() 未被识别 手动添加 queryRunner.createIndex()
列类型不匹配 TypeORM 推断错误 手动修改列类型定义

步骤 4:执行迁移

确认当前数据库状态(可选):

pnpm migration:show

运行迁移:

pnpm migration:run

预期输出:

query: SELECT * FROM "migrations" "migrations" ...
query: CREATE TABLE "[table_name]" ...
Migration [MigrationName] has been executed successfully.

步骤 5:验证迁移结果

检查表是否创建成功:

psql -h 127.0.0.1 -U postgres -c "\d+ [table_name]"

验证表结构:

  • 列名和类型正确
  • 主键存在
  • 外键约束正确
  • 索引已创建
  • 默认值设置正确

如涉及数据迁移,验证数据:

psql -h 127.0.0.1 -U postgres -c "SELECT COUNT(*) FROM [table_name];"

步骤 6:测试回滚(可选但推荐)

在开发环境中测试回滚:

pnpm migration:revert

验证表已删除/还原

重新运行迁移:

pnpm migration:run

常见场景

场景 A:新建实体的表迁移

  1. 实体已创建 → 执行步骤 2 生成迁移
  2. 审查迁移(通常无需修改)
  3. 运行并验证

场景 B:添加新列到现有表

  1. 修改实体添加 @Column()
  2. 生成迁移
  3. 审查 ALTER TABLE 语句
  4. 如涉及现有数据,考虑默认值
  5. 运行并验证

场景 C:带数据迁移的表结构变更

  1. 生成迁移(只含结构变更)
  2. 手动编辑迁移文件,在 up() 中添加数据迁移 SQL
  3. 示例:

    public async up(queryRunner: QueryRunner): Promise<void> {
    // 1. 结构变更
    await queryRunner.addColumn(
    'disabled_person',
    new TableColumn({
      name: 'new_field',
      type: 'varchar',
      length: '50',
    })
    );
    
    // 2. 数据迁移
    await queryRunner.query(`
    UPDATE disabled_person
    SET new_field = SUBSTRING(old_field, 1, 50)
    WHERE new_field IS NULL
    `);
    }
    

场景 D:重命名列或表

TypeORM 有时检测不到重命名,会生成 DROP + ADD。

手动优化:

// TypeORM 生成的(不理想)
public async up(queryRunner: QueryRunner): Promise<void> {
  await queryRunner.dropColumn('table', 'old_name');
  await queryRunner.addColumn('table', new TableColumn({ name: 'new_name', ... }));
}

// 手动优化为重命名
public async up(queryRunner: QueryRunner): Promise<void> {
  await queryRunner.renameColumn('table', 'old_name', 'new_name');
}

常见陷阱与解决方案

⚠️ 迁移文件命名规范(重要!)

问题:迁移文件必须以数字结尾才能被 Glob 模式匹配。

文件名 是否匹配 *[0-9].ts 说明
Migrate...1737260000000.ts ✅ 匹配 以数字结尾
1768968781678-Create...Table.ts ❌ 不匹配 以字母结尾

正确命名格式

[描述性名称][时间戳].ts

示例:CreateDisabledPersonPhoneTable1768968781678.ts

⚠️ 迁移类必须包含 name 属性

export class CreateDisabledPersonPhoneTable1768968781678 implements MigrationInterface {
  name = 'CreateDisabledPersonPhoneTable1768968781678';  // 必须有!

  public async up(queryRunner: QueryRunner): Promise<void> {
    // ...
  }
}

没有 name 属性会导致迁移无法被 TypeORM CLI 识别。

⚠️ 主键列名差异

问题:不同表可能使用不同的主键列名。

// ❌ 错误:假设所有表都用 id 作为主键
referencedColumnNames: ["id"]

// ✅ 正确:检查实际表的主键列名
// disabled_person 表的主键是 person_id,不是 id!
referencedColumnNames: ["person_id"]

解决方法:在创建外键前,先检查被引用表的实际主键列名:

psql -h 127.0.0.1 -U postgres -c "\d+ table_name"

⚠️ migration:generate 可能失败

问题:TypeORM 有时无法检测实体变化,报告 "No changes in database schema were found"。

可能原因

  1. 实体文件没有被 Glob 模式正确加载
  2. 实体与数据库表已完全同步
  3. TypeScript 编译问题

解决方案:手动创建迁移文件

import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm";

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

  public async up(queryRunner: QueryRunner): Promise<void> {
    // 手动编写迁移逻辑
    await queryRunner.createTable(
      new Table({
        name: "your_table",
        columns: [/* ... */],
      }),
      true
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.dropTable("your_table", true);
  }
}

⚠️ 更新 migrations/index.ts

创建迁移文件后,必须更新 packages/server/migrations/index.ts

import { YourMigrationTimestamp } from './YourMigrationTimestamp';

export const migrations: any[] = [
  MigrateNotWorkingToPreWorking1737260000000,
  YourMigrationTimestamp,  // 按时间戳顺序添加
]

注意事项

⚠️ 生产环境迁移

  1. 先在开发/测试环境验证
  2. 备份数据库
  3. 评估迁移时间(大表操作可能很慢)
  4. 准备回滚计划
  5. 在低峰时段执行

⚠️ 数据迁移

  1. 分批处理:大表数据迁移分批执行
  2. 事务控制:考虑是否需要关闭自动提交
  3. 验证脚本:先 SELECT 预览,再 UPDATE
  4. 保留原字段:先保留,验证无误后再清理

⚠️ 外键约束

  1. 先创建被引用的表
  2. CASCADE 设置要谨慎
  3. 考虑数据的完整性

参考命令

# 查看迁移状态
pnpm migration:show

# 生成迁移
pnpm migration:generate -- -n MigrationName

# 运行迁移
pnpm migration:run

# 回滚最后一次迁移
pnpm migration:revert

# 查看表结构
psql -h 127.0.0.1 -U postgres -c "\d+ table_name"

# 查看所有表
psql -h 127.0.0.1 -U postgres -c "\dt"

工作流完成

当所有步骤通过验证后,工作流完成。

在 Story 任务中记录:

- [x] Task: 数据库迁移
  - [x] 检查实体在 glob 覆盖范围内
  - [x] 使用 pnpm migration:generate 生成迁移
  - [x] 审查并调整迁移文件
  - [x] 运行 pnpm migration:run
  - [x] 验证表结构和数据