--- 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:前置检查 **检查实体文件是否已创建:** ```bash # 确认实体文件存在 ls allin-packages/[module-name]/src/entities/[entity-name].entity.ts ``` **检查实体是否在 Glob 覆盖范围内:** 查看 `packages/server/src/data-source-cli.ts` 确认实体路径已被包含: ```typescript entities: [ "../allin-packages/[your-module]/src/entities/*.ts", // 确认你的模块在这里 // ... ] ``` **检查表是否已存在于数据库:** ```bash psql -h 127.0.0.1 -U postgres -c "\dt [table_name]" ``` 如果表已存在,**停止工作流** - 考虑是否需要修改表而不是创建新表。 --- ### 步骤 2:生成迁移文件 **进入 server 目录:** ```bash cd packages/server ``` **运行 TypeORM 迁移生成命令:** ```bash pnpm migration:generate -- -n [MigrationName] ``` **迁移命名规范:** - 使用 PascalCase - 描述性名称,如 `CreateDisabledPersonPhoneTable` - 如涉及数据迁移,添加 `AndMigrateData` 后缀 **预期输出:** ``` Migration /path/to/packages/server/migrations/[timestamp]-[MigrationName].ts has been generated successfully. ``` --- ### 步骤 3:审查生成的迁移文件 **打开生成的迁移文件:** ```bash # 迁移文件位于 packages/migrations/[timestamp]-[MigrationName].ts ``` **检查清单:** - [ ] `up()` 方法包含正确的 `CREATE TABLE` 或 `ALTER TABLE` 语句 - [ ] `down()` 方法包含正确的回滚逻辑 - [ ] 列类型、约束、索引与实体定义一致 - [ ] 外键关系正确设置 - [ ] CASCADE 删除策略符合预期 **常见问题修复:** | 问题 | 原因 | 解决方案 | |------|------|---------| | 迁移为空 | 实体与数据库表已同步 | 检查是否真的需要迁移 | | 缺少索引 | 实体 `@Index()` 未被识别 | 手动添加 `queryRunner.createIndex()` | | 列类型不匹配 | TypeORM 推断错误 | 手动修改列类型定义 | --- ### 步骤 4:执行迁移 **确认当前数据库状态(可选):** ```bash pnpm migration:show ``` **运行迁移:** ```bash pnpm migration:run ``` **预期输出:** ``` query: SELECT * FROM "migrations" "migrations" ... query: CREATE TABLE "[table_name]" ... Migration [MigrationName] has been executed successfully. ``` --- ### 步骤 5:验证迁移结果 **检查表是否创建成功:** ```bash psql -h 127.0.0.1 -U postgres -c "\d+ [table_name]" ``` **验证表结构:** - [ ] 列名和类型正确 - [ ] 主键存在 - [ ] 外键约束正确 - [ ] 索引已创建 - [ ] 默认值设置正确 **如涉及数据迁移,验证数据:** ```bash psql -h 127.0.0.1 -U postgres -c "SELECT COUNT(*) FROM [table_name];" ``` --- ### 步骤 6:测试回滚(可选但推荐) **在开发环境中测试回滚:** ```bash pnpm migration:revert ``` **验证表已删除/还原** **重新运行迁移:** ```bash pnpm migration:run ``` --- ## 常见场景 ### 场景 A:新建实体的表迁移 1. 实体已创建 → 执行步骤 2 生成迁移 2. 审查迁移(通常无需修改) 3. 运行并验证 ### 场景 B:添加新列到现有表 1. 修改实体添加 `@Column()` 2. 生成迁移 3. 审查 `ALTER TABLE` 语句 4. 如涉及现有数据,考虑默认值 5. 运行并验证 ### 场景 C:带数据迁移的表结构变更 1. 生成迁移(只含结构变更) 2. 手动编辑迁移文件,在 `up()` 中添加数据迁移 SQL 3. 示例: ```typescript public async up(queryRunner: QueryRunner): Promise { // 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。 **手动优化:** ```typescript // TypeORM 生成的(不理想) public async up(queryRunner: QueryRunner): Promise { await queryRunner.dropColumn('table', 'old_name'); await queryRunner.addColumn('table', new TableColumn({ name: 'new_name', ... })); } // 手动优化为重命名 public async up(queryRunner: QueryRunner): Promise { await queryRunner.renameColumn('table', 'old_name', 'new_name'); } ``` --- ## 常见陷阱与解决方案 ### ⚠️ 迁移文件命名规范(重要!) **问题**:迁移文件必须以数字结尾才能被 Glob 模式匹配。 | 文件名 | 是否匹配 `*[0-9].ts` | 说明 | |--------|---------------------|------| | `Migrate...1737260000000.ts` | ✅ 匹配 | 以数字结尾 | | `1768968781678-Create...Table.ts` | ❌ 不匹配 | 以字母结尾 | **正确命名格式**: ``` [描述性名称][时间戳].ts ``` 示例:`CreateDisabledPersonPhoneTable1768968781678.ts` ### ⚠️ 迁移类必须包含 name 属性 ```typescript export class CreateDisabledPersonPhoneTable1768968781678 implements MigrationInterface { name = 'CreateDisabledPersonPhoneTable1768968781678'; // 必须有! public async up(queryRunner: QueryRunner): Promise { // ... } } ``` 没有 `name` 属性会导致迁移无法被 TypeORM CLI 识别。 ### ⚠️ 主键列名差异 **问题**:不同表可能使用不同的主键列名。 ```typescript // ❌ 错误:假设所有表都用 id 作为主键 referencedColumnNames: ["id"] // ✅ 正确:检查实际表的主键列名 // disabled_person 表的主键是 person_id,不是 id! referencedColumnNames: ["person_id"] ``` **解决方法**:在创建外键前,先检查被引用表的实际主键列名: ```bash 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 编译问题 **解决方案**:手动创建迁移文件 ```typescript import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm"; export class YourMigrationTimestamp implements MigrationInterface { name = 'YourMigrationTimestamp'; public async up(queryRunner: QueryRunner): Promise { // 手动编写迁移逻辑 await queryRunner.createTable( new Table({ name: "your_table", columns: [/* ... */], }), true ); } public async down(queryRunner: QueryRunner): Promise { await queryRunner.dropTable("your_table", true); } } ``` ### ⚠️ 更新 migrations/index.ts 创建迁移文件后,必须更新 `packages/server/migrations/index.ts`: ```typescript 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. **考虑数据的完整性** --- ## 参考命令 ```bash # 查看迁移状态 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 任务中记录:** ```yaml - [x] Task: 数据库迁移 - [x] 检查实体在 glob 覆盖范围内 - [x] 使用 pnpm migration:generate 生成迁移 - [x] 审查并调整迁移文件 - [x] 运行 pnpm migration:run - [x] 验证表结构和数据 ```