TypeORM Postgres 迁移实战:从配置到生产部署的完整指南
如果你正在使用 TypeORM 管理 PostgreSQL 数据库,并且希望像管理代码一样管理数据库模式变更,那么迁移(Migration)是你必须掌握的核心技能。本文将深入解析 TypeORM 迁移的配置参数、常见陷阱、生产环境最佳实践,并提供可复用的解决方案。
它解决什么问题 / 适用场景
TypeORM 迁移解决的核心问题是数据库模式变更的版本控制。当你的应用需要修改表结构(添加列、创建索引、修改约束)时,手动执行 SQL 脚本会导致以下问题:
- 不同环境(开发、测试、生产)的数据库结构不一致
- 无法追溯谁在什么时候做了什么修改
- 回滚操作困难,容易出错
最适合的场景:
| 场景 | 说明 |
|---|---|
| 团队协作开发 | 多个开发者同时修改实体,迁移文件可以像代码一样进行 Code Review 和版本控制 |
| 多环境部署 | 确保 CI/CD 管道、预发布环境、生产环境的数据库结构完全一致 |
| NestJS / Express 项目 | TypeORM 是这些框架中最流行的 ORM,迁移功能可以无缝集成 |
| 需要事务性迁移 | 当迁移操作涉及多个步骤时,migrationsTransactionMode 提供原子性保证 |
不适合的场景:
- 快速原型开发:早期迭代阶段使用
synchronize: true更便捷,但会牺牲版本控制 - 非关系型数据库:此设置专为 PostgreSQL 设计
- 与 Prisma 或 Sequelize 混合使用:不建议在同一个项目中混合使用多个 ORM 进行迁移管理
核心配置 / 参数说明
TypeORM 迁移的核心配置在 DataSource 对象中定义。以下是所有关键参数的详细说明:
TYPESCRIPT// data-source.ts import { DataSource } from "typeorm"; export const AppDataSource = new DataSource({ type: "postgres", host: process.env.DB_HOST || "localhost", port: parseInt(process.env.DB_PORT || "5432"), username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, entities: ["src/entities/**/*.ts"], // 以下是迁移相关配置 synchronize: false, // 必须设为 false migrations: ["src/migrations/**/*.ts"], // 迁移文件路径 migrationsRun: false, // 生产环境建议设为 false migrationsTableName: "migrations", // 存储迁移记录的表名 migrationsTransactionMode: "each", // 推荐生产环境使用 "each" });
参数详解
| 参数 | 是否必需 | 默认值 | 说明 |
|---|---|---|---|
synchronize | 是 | false | 必须设为 false。如果设为 true,TypeORM 会在每次应用启动时自动同步实体与数据库,这会覆盖迁移管理,导致版本控制失效。 |
migrations | 是 | [] | 定义需要加载的迁移文件列表。支持类名和目录(glob 模式)。例如:["src/migrations/**/*.ts"] 会匹配 src/migrations 目录下所有 .ts 文件。 |
migrationsRun | 否 | false | 是否在应用启动时自动运行迁移。生产环境建议设为 false,通过 CI/CD 管道手动触发。 |
migrationsTableName | 否 | 'migrations' | 存储已执行迁移记录的表名。TypeORM 会自动创建该表,记录每个迁移的执行时间戳和名称。 |
migrationsTransactionMode | 否 | 'all' | 控制迁移的事务模式。可选值:all(所有迁移在一个事务中)、each(每个迁移独立事务)、none(无事务)。 |
事务模式选择指南
TYPESCRIPT// 推荐的生产环境配置 migrationsTransactionMode: "each" // 仅当迁移数量少且执行快时使用 migrationsTransactionMode: "all" // 仅当迁移中包含 CREATE INDEX CONCURRENTLY 等不支持事务的 DDL 时使用 migrationsTransactionMode: "none"
与同类方案对比
| 维度 | TypeORM 迁移 | Prisma Migrate | Knex.js | 直接 SQL 脚本 |
|---|---|---|---|---|
| 语言/框架集成 | 深度集成 TypeORM,适合 NestJS/Express | 独立 CLI,与框架无关 | 独立库,与框架无关 | 与框架无关 |
| 迁移文件生成 | 自动生成(基于实体变化) | 自动生成(基于 Schema 变化) | 手动编写 | 手动编写 |
| 迁移文件格式 | TypeScript/JavaScript 类 | SQL 文件 | JavaScript/TypeScript | SQL 文件 |
| 事务支持 | 支持(all/none/each) | 支持(默认每个迁移一个事务) | 支持 | 手动管理 |
| 回滚能力 | 支持(revert 命令) | 支持(migrate down) | 支持(rollback) | 手动编写回滚脚本 |
| 学习曲线 | 中等 | 较低 | 中等 | 低(但维护成本高) |
TypeORM 迁移的独特优势:
- 与实体深度绑定:自动生成的迁移文件能精确反映实体变化,减少手动编写错误
- TypeScript 原生支持:在迁移文件中可以使用类型检查和 IDE 智能提示
- 灵活的事务模式:
migrationsTransactionMode提供了细粒度的事务控制
生产环境实践与注意事项
1. 并发冲突处理
问题:多个应用实例同时尝试运行迁移可能导致死锁或数据不一致。
解决方案:
TYPESCRIPT// 在 CI/CD 管道中,确保迁移只在一个实例上执行 // 例如,使用 Kubernetes Job: apiVersion: batch/v1 kind: Job metadata: name: run-migrations spec: template: spec: restartPolicy: Never containers: - name: migration image: your-app:latest command: ["npx", "typeorm", "migration:run", "-d", "./dist/data-source.js"]
2. 权限分离
问题:迁移用户需要 DDL 权限,这通常比应用用户权限更高。
最佳实践:
SQL-- 创建专用迁移用户 CREATE USER migration_user WITH PASSWORD 'secure_password'; GRANT ALL PRIVILEGES ON DATABASE your_database TO migration_user; -- 应用运行时使用受限用户 CREATE USER app_user WITH PASSWORD 'app_password'; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
3. 零停机迁移策略
核心原则:编写向后兼容的迁移。
TYPESCRIPT// 错误示例:直接删除列 export class RemoveOldColumn1234567890123 { async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "old_column"`); } async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(`ALTER TABLE "users" ADD COLUMN "old_column" varchar`); } } // 正确示例:分步迁移 // 第一步:添加新列 export class AddNewColumn1234567890123 { async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(`ALTER TABLE "users" ADD COLUMN "new_column" varchar DEFAULT 'default_value'`); } async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "new_column"`); } } // 第二步:更新应用代码使用新列 // 第三步:删除旧列(在下一个部署周期) export class RemoveOldColumn1234567890124 { async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "old_column"`); } async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(`ALTER TABLE "users" ADD COLUMN "old_column" varchar`); } }
4. 网络安全配置
TYPESCRIPT// 使用环境变量管理数据库凭证 // .env 文件 DB_HOST=your-db-host.rds.amazonaws.com DB_PORT=5432 DB_USERNAME=migration_user DB_PASSWORD=${DB_PASSWORD} // 从密钥管理服务获取 DB_DATABASE=your_database DB_SSL=true // data-source.ts export const AppDataSource = new DataSource({ type: "postgres", host: process.env.DB_HOST, port: parseInt(process.env.DB_PORT || "5432"), username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, ssl: process.env.DB_SSL === "true" ? { rejectUnauthorized: false } : false, // ... });
常见报错与排查
错误 1:QueryFailedError: relation "migrations" does not exist
原因:migrationsTableName 指定的表在数据库中不存在,且 migrationsRun 为 false 或首次运行。
解决方案:
BASH# 运行迁移命令,TypeORM 会自动创建 migrations 表 npx typeorm migration:run -d ./path/to/data-source.ts # 如果手动创建,确保表结构正确 CREATE TABLE IF NOT EXISTS "migrations" ( "id" SERIAL PRIMARY KEY, "timestamp" BIGINT NOT NULL, "name" VARCHAR(255) NOT NULL );
错误 2:Error: Cannot find module './migrations/TIMESTAMP-first-migration'
原因:migrations 配置中的路径或文件名不正确,或者迁移文件未被编译。
解决方案:
BASH# 检查迁移文件路径是否正确 ls -la src/migrations/ # 如果使用 TypeScript,确保编译后再运行 npx tsc npx typeorm migration:run -d ./dist/data-source.js # 或者使用 ts-node 直接运行 npx ts-node -r tsconfig-paths/register ./node_modules/.bin/typeorm migration:run -d ./src/data-source.ts
错误 3:QueryFailedError: column "xxx" of relation "yyy" does not exist
原因:迁移文件试图操作一个不存在的列,通常是因为实体定义与数据库实际结构不一致。
解决方案:
BASH# 检查当前数据库状态 psql -d your_database -c "\d yyy" # 重新生成迁移文件 npx typeorm migration:generate -d ./src/data-source.ts -n SyncEntityChanges
错误 4:Error: DataSource is not set for this entity.
原因:在运行迁移时,TypeORM 无法找到与实体关联的 DataSource。
解决方案:
TYPESCRIPT// 确保 DataSource 配置中正确注册了所有实体 export const AppDataSource = new DataSource({ // ... entities: ["src/entities/**/*.ts"], // 确保路径正确 }); // 运行迁移时使用正确的 -d 参数 npx typeorm migration:run -d ./src/data-source.ts
常见问题 FAQ
Q: 如何在生产环境中安全地运行 TypeORM 迁移,而不会导致应用停机?
A: 生产环境中安全运行迁移的关键是零停机部署:
-
分离迁移与启动:不要将
migrationsRun: true设置为应用启动的一部分。迁移应作为 CI/CD 管道中的一个独立步骤执行。 -
向后兼容:编写迁移时,确保新的数据库模式与旧的应用代码兼容。例如:
- 添加列时,使用
ALTER TABLE ... ADD COLUMN ... DEFAULT ...提供默认值 - 重命名列时,先添加新列,然后逐步更新应用代码,最后再删除旧列
- 添加列时,使用
-
使用蓝绿部署或滚动更新:
- 先运行迁移,更新数据库
- 然后逐步将流量切换到新版本的应用
- 如果新版本出现问题,可以快速回滚到旧版本(前提是迁移是向后兼容的)
-
监控与回滚:在迁移执行期间和之后,密切监控应用错误率和数据库性能。准备好回滚脚本(
typeorm migration:revert)。
Q: 我应该在 migrationsTransactionMode 中使用 all、each 还是 none?
A: 选择取决于迁移的复杂性和风险:
-
all(默认):将所有迁移包裹在一个数据库事务中。如果任何一个迁移失败,所有更改都会回滚。这提供了最强的原子性保证,但风险很高:如果迁移数量多或耗时长,事务可能持有锁很长时间,导致数据库性能下降甚至死锁。适用于:迁移数量少、执行快、且需要完全原子性的场景。 -
each:每个迁移都在自己的事务中运行。如果一个迁移失败,只有该迁移的更改会回滚,之前成功的迁移保持不变。这是推荐的生产环境选项,因为它平衡了安全性和性能。适用于:大多数生产环境。 -
none:不启用任何事务。每个 SQL 语句自动提交。风险最高:如果迁移中途失败,数据库可能处于不一致状态。仅适用于:迁移中使用了不支持事务的 DDL 语句(如CREATE INDEX CONCURRENTLY)或需要手动控制事务的场景。
Q: 如何处理 TypeORM 迁移中的冲突(例如,两个开发者同时生成了相同时间戳的迁移文件)?
A: 这是团队协作中常见的问题。以下是几种解决方案:
-
使用时间戳 + 描述:TypeORM 默认使用时间戳(如
TIMESTAMP-first-migration),但建议在文件名中加入简短描述(如20231027120000-add-user-email-column.ts),以减少冲突概率。 -
使用 Git 工作流:
- 每个开发者从最新的
main分支创建功能分支 - 在功能分支上生成迁移文件
- 合并前,先
rebase到最新的main分支,解决任何迁移文件冲突 - 冲突通常发生在文件名上,可以通过重命名文件或调整时间戳来解决
- 每个开发者从最新的
-
使用迁移 ID 生成策略:一些团队使用递增的数字(如
001-initial-schema.ts、002-add-users-table.ts)而不是时间戳,但这需要手动管理。 -
CI/CD 检查:在 CI 中运行
typeorm migration:run来验证所有迁移是否都能成功执行,确保合并前没有冲突。