TypeORM Postgres 迁移实战:从配置到生产部署的完整指南

主题: typeorm-postgres-migration-setup更新于: 2026/6/27作者:AgentFactory 技术团队

如果你正在使用 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"
});

参数详解

参数是否必需默认值说明
synchronizefalse必须设为 false。如果设为 true,TypeORM 会在每次应用启动时自动同步实体与数据库,这会覆盖迁移管理,导致版本控制失效。
migrations[]定义需要加载的迁移文件列表。支持类名和目录(glob 模式)。例如:["src/migrations/**/*.ts"] 会匹配 src/migrations 目录下所有 .ts 文件。
migrationsRunfalse是否在应用启动时自动运行迁移。生产环境建议设为 false,通过 CI/CD 管道手动触发。
migrationsTableName'migrations'存储已执行迁移记录的表名。TypeORM 会自动创建该表,记录每个迁移的执行时间戳和名称。
migrationsTransactionMode'all'控制迁移的事务模式。可选值:all(所有迁移在一个事务中)、each(每个迁移独立事务)、none(无事务)。

事务模式选择指南

TYPESCRIPT
// 推荐的生产环境配置
migrationsTransactionMode: "each"

// 仅当迁移数量少且执行快时使用
migrationsTransactionMode: "all"

// 仅当迁移中包含 CREATE INDEX CONCURRENTLY 等不支持事务的 DDL 时使用
migrationsTransactionMode: "none"

与同类方案对比

维度TypeORM 迁移Prisma MigrateKnex.js直接 SQL 脚本
语言/框架集成深度集成 TypeORM,适合 NestJS/Express独立 CLI,与框架无关独立库,与框架无关与框架无关
迁移文件生成自动生成(基于实体变化)自动生成(基于 Schema 变化)手动编写手动编写
迁移文件格式TypeScript/JavaScript 类SQL 文件JavaScript/TypeScriptSQL 文件
事务支持支持(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 指定的表在数据库中不存在,且 migrationsRunfalse 或首次运行。

解决方案

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: 生产环境中安全运行迁移的关键是零停机部署

  1. 分离迁移与启动:不要将 migrationsRun: true 设置为应用启动的一部分。迁移应作为 CI/CD 管道中的一个独立步骤执行。

  2. 向后兼容:编写迁移时,确保新的数据库模式与旧的应用代码兼容。例如:

    • 添加列时,使用 ALTER TABLE ... ADD COLUMN ... DEFAULT ... 提供默认值
    • 重命名列时,先添加新列,然后逐步更新应用代码,最后再删除旧列
  3. 使用蓝绿部署或滚动更新

    • 先运行迁移,更新数据库
    • 然后逐步将流量切换到新版本的应用
    • 如果新版本出现问题,可以快速回滚到旧版本(前提是迁移是向后兼容的)
  4. 监控与回滚:在迁移执行期间和之后,密切监控应用错误率和数据库性能。准备好回滚脚本(typeorm migration:revert)。

Q: 我应该在 migrationsTransactionMode 中使用 alleach 还是 none

A: 选择取决于迁移的复杂性和风险:

  • all(默认):将所有迁移包裹在一个数据库事务中。如果任何一个迁移失败,所有更改都会回滚。这提供了最强的原子性保证,但风险很高:如果迁移数量多或耗时长,事务可能持有锁很长时间,导致数据库性能下降甚至死锁。适用于:迁移数量少、执行快、且需要完全原子性的场景。

  • each:每个迁移都在自己的事务中运行。如果一个迁移失败,只有该迁移的更改会回滚,之前成功的迁移保持不变。这是推荐的生产环境选项,因为它平衡了安全性和性能。适用于:大多数生产环境。

  • none:不启用任何事务。每个 SQL 语句自动提交。风险最高:如果迁移中途失败,数据库可能处于不一致状态。仅适用于:迁移中使用了不支持事务的 DDL 语句(如 CREATE INDEX CONCURRENTLY)或需要手动控制事务的场景。

Q: 如何处理 TypeORM 迁移中的冲突(例如,两个开发者同时生成了相同时间戳的迁移文件)?

A: 这是团队协作中常见的问题。以下是几种解决方案:

  1. 使用时间戳 + 描述:TypeORM 默认使用时间戳(如 TIMESTAMP-first-migration),但建议在文件名中加入简短描述(如 20231027120000-add-user-email-column.ts),以减少冲突概率。

  2. 使用 Git 工作流

    • 每个开发者从最新的 main 分支创建功能分支
    • 在功能分支上生成迁移文件
    • 合并前,先 rebase 到最新的 main 分支,解决任何迁移文件冲突
    • 冲突通常发生在文件名上,可以通过重命名文件或调整时间戳来解决
  3. 使用迁移 ID 生成策略:一些团队使用递增的数字(如 001-initial-schema.ts002-add-users-table.ts)而不是时间戳,但这需要手动管理。

  4. CI/CD 检查:在 CI 中运行 typeorm migration:run 来验证所有迁移是否都能成功执行,确保合并前没有冲突。

相关深度解决方案

在配置当前服务时,如果您遇到了数据库锁死或需要更高并发的读写控制,建议配合参考我们整理的 SQLite MCP 服务的高级缓存配置指南 来提升响应速度。