TypeORM 手工编写迁移(Migration)全指南:migration:create 与 up/down 实战
在 TypeORM 中,迁移(Migration)是"一个包含 SQL 查询、用于更新数据库 schema 的单文件"。当项目上线数月、数据库里已沉淀真实数据后,直接依赖 synchronize: true 自动同步结构是危险的,迁移则提供了版本化、可回滚、按时间戳顺序执行的安全变更途径。本文围绕官方文档 docs/docs/migrations/03-creating.md 展开,讲解如何用 migration:create 手工创建迁移文件、理解 up / down 方法职责、借助 QueryRunner 编写与回滚 SQL,并结合仓库源码(MigrationCreateCommand.ts、MigrationInterface.ts、MigrationExecutor.ts)还原 CLI 到底做了什么、执行时如何被调度。读完你将能独立手工产出规范、可运行的 TypeORM 迁移并理解其底层运行机制。
为什么需要手工创建迁移
在生产环境中,"改实体代码让 TypeORM 自动同步数据库"并非好主意:一旦数据库已存在数据,synchronize: true 可能触发破坏性的结构变更,且没有任何版本与回滚痕迹。迁移把数据库演进变成一串有名字、有时间戳、可追溯的文件——每次发布对应一个迁移,先在开发库验证,再按序执行到生产库。官方背景见 docs/docs/migrations/01-why.md。
典型场景正如原文档所述:假设线上已有运行数月的 Post 实体,字段是 title,现在新版本需要把列改名为 name。数据库里有成千上万条 post 记录,不能简单删除重建。这时最稳妥的做法就是新增一条迁移,执行(PostgreSQL 方言):
ALTER TABLE "post" RENAME COLUMN "title" TO "name";
TypeORM 提供的"迁移"正是让你安全书写并运行这类 SQL 的地方,而 migration:create 则是手工开启这一过程的入口。
使用 migration:create 生成迁移骨架
基本命令与产物
TypeORM 提供 migration:create 命令,通过指定迁移名称与存放目录来生成一个空迁移文件:
npx typeorm migration:create <path/to/migrations>/<migration-name>
例如:
npx typeorm migration:create src/db/migrations/post-refactoring
执行完成后,src/db/migrations 目录下会新增一个命名为 {TIMESTAMP}-post-refactoring.ts 的文件,其中 {TIMESTAMP} 是生成文件时当前时间戳(毫秒级),用于保证迁移文件的全局唯一顺序。从 MigrationCreateCommand.ts 的 handler 实现可以看到具体的组装逻辑:
const timestamp = CommandUtils.getTimestamp(args.timestamp)
const fullPath =
path.dirname(inputPath) + "/" + timestamp + "-" + filename
即文件名 = 目录 + "/" + 时间戳 + "-" + 你传入的名称,随后通过 CommandUtils.createFile 递归创建目录并写出文件(见 CommandUtils.ts)。
需要特别说明的是:命令中的 <migration-name> 仅是"人类可读后缀",真正的顺序权威是前面的时间戳。所以请勿手改时间戳前缀来"人工排序";若确有需要控制时间戳,可使用下面的 --timestamp 参数。
生成的骨架内容
打开生成的文件,你会看到如下内容:
import { MigrationInterface, QueryRunner } from "typeorm"
export class PostRefactoringTIMESTAMP implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {}
async down(queryRunner: QueryRunner): Promise<void> {}
}
对照源码中 getTemplate,骨架由三部分构成:
- 导入
MigrationInterface与QueryRunner; - 类名 = 对传入名称做 camelCase 处理(首字母大写)后再拼接时间戳。源码为
export class ${camelCase(name, true)}${timestamp},其中camelCase定义于 StringUtils.ts。因此post-refactoring会变成PostRefactoring,最终类名为PostRefactoring1700000000000; - 类实现了
MigrationInterface,内含两个待填充的异步方法up与down,它们都接收一个QueryRunner参数。
提示:迁移类名中的时间戳来自 Migration.ts 所描述的
timestamp字段语义——它指示了迁移的执行顺序,TypeORM 运行时正是依据它排序、并判断哪些迁移已经执行过。因此请保持类名与文件名的一致性,不要随意改动类名后缀。
up 与 down:正向执行与逆向回滚
生成的骨架中有两个必须填充代码的方法:
up:包含执行迁移所需的全部代码(建表、加列、改名、改约束、DML 数据订正等);down:回滚up所做的一切变更,用于撤销上一次迁移。
从接口定义看(MigrationInterface.ts),up 与 down 都接收 QueryRunner 并返回 Promise:
export interface MigrationInterface {
up(queryRunner: QueryRunner): Promise<any>
down(queryRunner: QueryRunner): Promise<any>
}
这两个方法的契约可以概括为"互为逆操作":up 如何把 schema 从 A 变为 B,down 就必须把 schema 从 B 准确地还原为 A。写 down 时务必针对 up 的每一条语句给出对称的反向语句。
通过 QueryRunner 执行数据库操作
在 up / down 内部,所有数据库操作都通过 QueryRunner 对象完成。它是 TypeORM 中"单一数据库连接上执行查询"的抽象:既支持 queryRunner.query(...) 运行原生 SQL,也支持 queryRunner.createTable / addColumn / renameColumn / dropColumn 等高级 schema 操作 API。关于 QueryRunner 的完整能力(如事务控制、流式查询、表/列/索引/外键管理方法),见 docs/docs/query-runner.md。
迁移执行器对 up 的调度逻辑位于 MigrationExecutor.ts:在确保 migrations 记录表存在后,依次调用 queryRunner.beforeMigration() → migration.instance.up(queryRunner) → queryRunner.afterMigration() → insertExecutedMigration(...)。这也就是说,你的 up 方法返回的 Promise 被 resolve 后,该迁移才会被记为"已执行";任何抛错都会中断流程并触发事务回滚(取决于事务模式),避免出现"跑了一半、记录却说没跑"的脏状态。
完整示例:重命名 Post 表列
沿用原文档 Post 实体字段 title → name 的改造,迁移内容如下:
import { MigrationInterface, QueryRunner } from "typeorm"
export class PostRefactoringTIMESTAMP implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "post" RENAME COLUMN "title" TO "name"`,
)
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "post" RENAME COLUMN "name" TO "title"`,
) // reverts things made in "up" method
}
}
注意点:
- SQL 是目标数据库方言——示例为 PostgreSQL,因此表名/列名带双引号。若你使用 MySQL / SQLite / SQL Server / Oracle 等,需要改成对应方言(例如 MySQL 使用反引号
`post`)。从驱动实现看,TypeORM 只是把字符串透传给底层驱动,方言正确性由你负责; - 如果
up里做了多条变更,down里要以相反顺序逐条还原,确保中途失败也不会残留不一致; - 强烈建议为每个迁移编写一条"目标可逆"的完整上下对应,保证未来
migration:revert可用(见 docs/docs/migrations/06-reverting.md)。
migration:create 的扩展选项
虽然原文档只演示了最简形式,MigrationCreateCommand.ts 中还暴露了三个实用选项,理解它们能更好地应对不同工程形态:
| 选项 | 别名 | 类型 | 默认值 | 作用 |
|---|---|---|---|---|
-o |
--outputJs |
boolean | false |
生成 JavaScript(.js)而非 TypeScript(.ts)迁移文件 |
--esm |
— | boolean | false |
与 -o 搭配,生成 ESM 风格(export class)而非 CommonJS(module.exports =) |
-t |
--timestamp |
number | 当前时间 | 为迁移指定自定义时间戳,需为非负数,否则抛出 TypeORMError: timestamp option should be a non-negative number |
示例:
# 生成 JS(CommonJS)迁移
npx typeorm migration:create -o src/db/migrations/post-refactoring
# 生成 JS(ESM)迁移
npx typeorm migration:create -o --esm src/db/migrations/post-refactoring
# 用自定义时间戳生成
npx typeorm migration:create -t 1700000000000 src/db/migrations/post-refactoring
从 getJavascriptTemplate 可以看出,JS 迁移同样实现 MigrationInterface 语义:up(queryRunner) 与 down(queryRunner) 方法签名不变,仅省略了 TypeScript 类型标注,并用 export class(ESM)或 module.exports =(CommonJS)导出。
为什么需要 -o?TS 迁移的运行前提
一个常见误区:migration:create / migration:generate 默认产出的是 .ts 文件,而 migration:run 与 migration:revert 只直接消费 .js 文件。因此直接跑 migration:run 前通常需要:
- 用
tsc把 TS 迁移编译为 JS;或 - 使用
ts-node相关入口运行.ts迁移,例如(详见 docs/docs/migrations/05-executing.md):
# CommonJS 项目
npx typeorm-ts-node-commonjs migration:run -- -d path-to-datasource-config
# ESM 项目
npx typeorm-ts-node-esm migration:run -- -d path-to-datasource-config
这就是 -o 的典型用武之地:某些不经过 TS 编译的部署环境(如纯 JS 工程)可直接生成 .js 迁移省去编译环节。
从源码看迁移从"手工文件"到"被记录"的完整链路
将手工创建的迁移投入实际使用前,还需在 DataSource 配置中注册它们——官方 Setup 指南见 docs/docs/migrations/02-setup.md:
export default new DataSource({
synchronize: false, // 必须关闭自动同步,否则迁移无意义
migrations: [__dirname + "/migrations/**/*{.js,.ts}"], // glob 加载目录
// 可选
migrationsRun: false, // 是否每次启动自动执行未运行迁移,默认 false
migrationsTableName: "migrations", // 记录已执行迁移的表名,默认 "migrations"
migrationsTransactionMode: "all", // all | none | each
})
跑 migration:run 后,MigrationExecutor 会负责(节选关键事实):
- 加载
migrations目录下全部迁移类,通过getMigrations()按时间戳排序(L587); - 用
getExecutedMigrations()对照记录表,筛出"待执行"集合getPendingMigrations()(L114-L127)——比对键是迁移的name(即类名),所以文件名、类名、时间戳必须保持稳定,改名即被视为"新迁移"; - 依据
migrationsTransactionMode(all默认整批单事务 /each每条一个事务 /none无事务)与迁移上可选的transaction字段决定事务边界(相关逻辑见 L309-L357); - 逐个调用
up,成功后把迁移写入记录表(insertExecutedMigration)。
因此手工创建迁移时务必记住这条链路隐含的纪律:
- 时间戳决定顺序:不要生成多个同毫秒时间戳的迁移并期望稳定顺序;
- 类名不可变:记录表按类名比对,改名会重复执行或失去回滚定位;
- down 必须可用:记录表存在是
migration:revert能精准撤销"最后一条"的前提,而down的质量决定回滚是否真正还原数据库。
手工创建 vs 自动生成
TypeORM 还提供 migration:generate,可根据实体与数据库的差异自动生成迁移文件,适用于结构变更来自实体代码的场景,详见 docs/docs/migrations/04-generating.md。两者定位不同:
| 维度 | migration:create(手工) | migration:generate(自动) |
|---|---|---|
| 适用场景 | 数据订正、索引调整、方言特殊 SQL、多步自定义逻辑 | 实体字段增删改、普通结构同步 |
| 内容来源 | 开发者书写 | 比较实体元数据与数据库后生成 |
| 控制粒度 | 完全可控、可读性强 | 一次生成大批语句,需人工 review |
实践中常组合使用:复杂的数据迁移与业务订正用手工 migration:create 精确编写,常规结构演进交给 migration:generate 后人工复核。若你对迁移运行后的校验与展示感兴趣,可继续阅读 docs/docs/migrations/07-status.md(查看执行状态)、docs/docs/migrations/08-faking.md(跳过执行直接标记)以及 docs/docs/migrations/09-api.md(编程式迁移 API)。
小结与最佳实践
手工创建迁移是 TypeORM 数据库演进体系的基石,核心要点可归纳为:
- 用
npx typeorm migration:create <path>/<name>生成{TIMESTAMP}-<name>.ts骨架; - 在
up中书写正向变更 SQL、在down中书写严格对称的回滚 SQL,全部经QueryRunner执行; - 牢记 CLI 默认产出 TS 文件,
migration:run/migration:revert需要.js(先编译或用 ts-node 入口); - 保持时间戳与类名的稳定,它们是排序与去重的唯一依据;
- 关闭
synchronize并在DataSource的migrations中注册目录(或具体类),随后即可用typeorm migration:run -- -d <datasource>将 schema 推进到目标版本。
掌握了 migration:create 与 up / down 的写法,你就掌握了在生产数据库上"安全前进、按需后退"的基本功,可配合官方 docs/docs/migrations/ 系列文档继续深入生成、执行、回滚与状态查看的完整迁移工作流。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00