TypeORM 自动生成迁移指南:用 migration:generate 从实体变更一键产出可回滚 SQL
TypeORM 提供了一条「自动生成迁移」的捷径:你只需要像平时开发一样修改实体(Entity),命令行工具会自动把实体与数据库中现有 Schema 做对比,把必须执行的 SQL 全部写进一个新迁移文件。本文基于 docs/docs/migrations/04-generating.md 展开,结合仓库中 MigrationGenerateCommand.ts 的实现与命令级测试,讲清 typeorm migration:generate 的完整用法、生成文件的结构,以及 --outputJs、--pretty、--dryrun、--check 等选项在真实工作流中的价值。读完你就能把「手工编写 ALTER TABLE」这一步从日常迭代中彻底去掉。
自动生成迁移做了什么
TypeORM 自动生成迁移(Automatic migration generation)的核心逻辑是差值对比:
- 读取你代码里当前定义的实体(Entity)与视图(View);
- 连接目标数据库,读取它当前实际存在的表结构与视图;
- 对比两边的差异(新增表、改名列、改类型、加索引、删约束……);
- 把差异换算成一组「升级 SQL」(up 查询)和一组「回滚 SQL」(down 查询);
- 以
{TIMESTAMP}-{migration-name}.ts的形式生成新的迁移文件,写入全部需要执行的 SQL。
如果没有任何差异,命令不会生成空文件,而是以退出码 1 结束,并提示你改用 migration:create 创建空白迁移手动填充——这个行为让 migration:generate 天然可以嵌入 CI,当作「数据库漂移检测器」使用。
从源码看对比过程
命令真正执行的底层调用链在 MigrationGenerateCommand.ts:
const sqlInMemory = await dataSource.driver
.createSchemaBuilder()
.log()
不同驱动的 createSchemaBuilder()(参见 Driver.ts 的接口声明,以及 postgres、mysql、mssql、oracle、better-sqlite3 等各驱动目录下的实现)会返回对应的 Schema Builder;对关系型数据库而言,实际执行对比的是 RdbmsSchemaBuilder.ts 中的 log() 方法:
async log(): Promise<SqlInMemory> {
this.queryRunner = this.dataSource.createQueryRunner()
try {
const tablePaths = ...
this.tables = await this.queryRunner.getTables(tablePaths) // 读取数据库现有表
this.views = await this.queryRunner.getViews(viewPaths) // 读取数据库现有视图
this.queryRunner.enableSqlMemory() // 开启 SQL 内存捕获
await this.executeSchemaSyncOperationsInProperOrder() // 按顺序计算并"执行"同步操作
return this.queryRunner.getMemorySql() // 取回 up/down 两组 SQL
} finally { ... }
}
它先把数据库里的真实表/视图结构取出来,再以「SQL 内存模式」跑一遍 schema 同步逻辑,最终返回一个包含 upQueries 与 downQueries 的 SqlInMemory 对象——这就是后面要写进迁移文件的两份 SQL 清单。整个过程并不会真正改动数据库,因此非常安全。
需要留意的一点是:在执行前,命令会对加载进来的 DataSource 强制改写选项(MigrationGenerateCommand.ts):
dataSource.setOptions({
synchronize: false,
migrationsRun: false,
dropSchema: false,
logging: false,
})
也就是说,即使你本地的 DataSource 配置了 synchronize: true 或 migrationsRun: true,在生成迁移时也都会被强制关闭,避免自动同步干扰差值计算,也不会在生成过程中误跑历史迁移。
命令语法与参数说明
生成迁移的基础命令如下:
typeorm migration:generate -d <path/to/datasource> <migration-name>
其中:
<path/to/datasource>:-d参数的值必须指向定义了你 DataSource 实例的文件路径。关于 DataSource 的定义方式可参考 DataSource 指南。<migration-name>:本次迁移的名字,会拼在时间戳后面构成文件名与类名。
文档给出的三种等价写法
省略 typeorm 前缀的方式,直接生成:
typeorm migration:generate -d <path/to/datasource> <migration-name>
也可以通过 --name 参数指定名称:
typeorm migration:generate -- -d <path/to/datasource> --name=<migration-name>
或者直接使用完整路径(名称前的目录会作为迁移文件的输出目录):
typeorm migration:generate -d <path/to/datasource> <path/to/migrations>/<migration-name>
如果迁移文件放在 src/db/migrations 下、名为 post-refactoring,那么实际运行可能长这样:
typeorm migration:generate -d src/data-source.ts src/db/migrations/post-refactoring
生成结果是一个名为 {TIMESTAMP}-post-refactoring.ts 的文件,其中 {TIMESTAMP} 是生成时刻的时间戳(默认取 Date.now())。
当前 CLI 实现中的参数一览
从当前 CLI 的源码定义(MigrationGenerateCommand.ts)看,命令签名为 migration:generate <path>,迁移名/路径是一个必填的 positional 参数;完整支持以下选项:
| 选项 | 别名 | 类型 | 默认值 | 作用 |
|---|---|---|---|---|
<path>(位置参数) |
— | string | 必填 | 迁移文件路径(可只给名字,也可给 目录/名字) |
--dataSource |
-d |
string | 必填 | 定义 DataSource 实例的文件路径 |
--outputJs |
-o |
boolean | false |
输出 JavaScript(.js)而非 TypeScript(.ts) |
--esm |
— | boolean | false |
与 -o 配合,输出 ESM 语法而非 CommonJS |
--pretty |
-p |
boolean | false |
对生成的 SQL 做多行格式化,方便阅读 |
--dryrun |
-dr |
boolean | false |
只把迁移内容打印到终端,不写文件 |
--check |
-ch |
boolean | false |
校验数据库是否与实体一致,CI 场景下非常有用 |
--timestamp |
-t |
number | 当前时间 | 为迁移文件指定自定义时间戳 |
其中 --timestamp 会被 CommandUtils.ts 中的 getTimestamp() 校验并处理:传入值必须是非负数字,否则会抛出 timestamp option should be a non-negative number 错误;不传则回退到 Date.now()。
完整示例:把 Post 实体字段 title 改名为 name
假设你有一个带 title 列的 Post 实体,这次你把它改名为 name:
// 修改前
export class Post {
@PrimaryGeneratedColumn()
id: number
@Column()
title: string
}
// 修改后
export class Post {
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
}
运行生成命令(此处以 post-refactoring 作为迁移名):
typeorm migration:generate -d <path/to/datasource> post-refactoring
TypeORM 会生成新文件 {TIMESTAMP}-post-refactoring.ts,内容大致如下:
import { MigrationInterface, QueryRunner } from "typeorm"
export class PostRefactoringTIMESTAMP implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "post" ALTER COLUMN "title" RENAME TO "name"`,
)
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "post" ALTER COLUMN "name" RENAME TO "title"`,
)
}
}
可以看到 up 负责把 Schema 升级到新状态,down 是它的镜像,负责回滚——这两份 SQL 都是 TypeORM 依据实体与数据库的差值自动算出来的,不需要你手工编写。当然,具体 SQL 文案会随数据库方言而不同(PostgreSQL、MySQL、SQL Server、SQLite 等各有差异),上例是文档给出的示意,真实执行时请以生成结果为准。
整个迁移文件的拼装逻辑可以在源码模板方法里看到:类名由 camelCase(migrationName, true) + timestamp 组成,up 的查询顺序与 down 的查询顺序互为逆序(MigrationGenerateCommand.ts),生成出的类还带有 name = '{ClassName}' 属性供 TypeORM 识别迁移身份:
export class PostRefactoring1699000000000 implements MigrationInterface {
name = 'PostRefactoring1699000000000'
public async up(queryRunner: QueryRunner): Promise<void> {
// upSqls...
}
public async down(queryRunner: QueryRunner): Promise<void> {
// downSqls(顺序已反转)
}
}
每条 SQL 在写入模板前还会经过 escapeTemplateLiteral() 处理:它会转义反斜杠、反引号与 ${,防止数据库元数据(比如列注释 COMMENT、默认值 DEFAULT)里的特殊字符破坏 JavaScript 模板字符串的语法。仓库里对应的 migration-generate-escape 测试(见 test/functional/commands)就是专门覆盖这类边界情况的。
面向纯 JavaScript 项目:把迁移输出为 JS 文件
如果项目是纯 JavaScript,没有安装 TypeScript 相关依赖,可以用 -o(--outputJs 的别名)把迁移输出为 JavaScript 文件。命令如下:
typeorm migration:generate -d <path/to/datasource> -o post-refactoring
执行后生成 {TIMESTAMP}-PostRefactoring.js,默认采用 CommonJS 写法,并带上 JSDoc 类型标注:
/**
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
* @typedef {import('typeorm').QueryRunner} QueryRunner
*/
/**
* @class
* @implements {MigrationInterface}
*/
module.exports = class PostRefactoringTIMESTAMP {
/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(
`ALTER TABLE "post" ALTER COLUMN "title" RENAME TO "name"`,
)
}
/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(
`ALTER TABLE "post" ALTER COLUMN "name" RENAME TO "title"`,
)
}
}
如果你的 JavaScript 项目使用 ESM 模块体系,可再叠加 --esm 标志生成 export 语法的版本:
typeorm migration:generate -d <path/to/datasource> -o --esm post-refactoring
/**
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
* @typedef {import('typeorm').QueryRunner} QueryRunner
*/
/**
* @class
* @implements {MigrationInterface}
*/
export class PostRefactoringTIMESTAMP {
/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(
`ALTER TABLE "post" ALTER COLUMN "title" RENAME TO "name"`,
)
}
/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(
`ALTER TABLE "post" ALTER COLUMN "name" RENAME TO "title"`,
)
}
}
源码中的 getJavascriptTemplate() 正是通过一行 const exportMethod = esm ? "export" : "module.exports =" 来切换两种模块格式的,其余结构(JSDoc 注释、up/down、name 属性)保持一致。
没有差异时怎么办:退出码与空迁移兜底
自动生成的前提是「实体与数据库之间存在差异」。如果你把实体改动同步进了数据库(比如开启了 synchronize),或者确实没改任何结构,命令会发现 upSqls 为空,此时:
-
普通模式下,终端输出黄色提示:
No changes in database schema were found - cannot generate a migration. To create a new empty migration use "typeorm migration:create" command并以退出码
1结束; -
若此时确实需要一条迁移,请改用 手动创建迁移 中的
typeorm migration:create,得到空白的up/down骨架后自行填写 SQL; -
该行为同时意味着:把它放进 CI,任何「实体已改但没补迁移」的提交都会让流水线失败,从而守住「数据库变更必须有迁移文件」这条纪律。
其他实用选项与推荐工作流
--pretty:SQL 多行格式化
默认情况下,一句复杂的 DDL 会作为一条长模板字符串写进迁移文件,可读性较差。加上 -p(--pretty 别名)后,生成前会调用 prettifyQuery() 把 SQL 拆成带缩进的多行,再嵌入迁移文件:
typeorm migration:generate -p -d <path/to/datasource> <migration-name>
--dryrun:只打印不落盘
在改动实体较多、想先预览一下会生成哪些 SQL 时,用 -dr 先跑一遍:
typeorm migration:generate --dryrun -d <path/to/datasource> <migration-name>
终端会把迁移文件内容完整打印出来(不写文件),确认无误后再去掉 --dryrun 正式生成。它与仓库中 schema:log 命令(见 SchemaLogCommand.ts)走的是同一条 createSchemaBuilder().log() 对比管线。
--check:数据库是否与实体一致
--check(别名 -ch)适合放进 CI 或代码提交前检查:
- 当没有任何差异时,输出绿色
No changes in database schema were found,进程以退出码0正常结束; - 当存在预期之外的差异时,输出黄色提示并把完整迁移内容打印出来,进程以退出码
1结束(MigrationGenerateCommand.ts)。
也就是说,migration:generate --check 是「这次改动是否需要迁移」的判断题,配合 查看迁移状态 的 migration:show 一起使用,可以组成完整的 Schema 变更守卫。
--timestamp:自定义时间戳
需要精确控制迁移文件命名中的时间戳(例如与发布版本对齐)时,可用数字毫秒值指定:
typeorm migration:generate -d <path/to/datasource> -t 1610975184784 post-refactoring
它会生成形如 1610975184784-post-refactoring.ts 的文件。传入的值必须是合法的非负数字,否则 getTimestamp() 会直接抛错,这也是一个不错的参数校验范例。
推荐的迭代节奏
一个值得固化成习惯的规则是:每当你对模型(实体)做一次改动,就立即生成一条迁移。一次改一个模型、生成一条迁移,可以让每条迁移文件都小而聚焦,便于 review 与排障,也让 down 回滚的粒度保持清晰。生成的迁移通过 执行迁移 的 migration:run 应用,出错时用 回滚迁移 的 migration:revert 撤销;对单条迁移的精细控制还可以参考 迁移 API。
测试如何保证生成正确
仓库对迁移生成逻辑有专门的命令级测试 test/functional/commands/migration-generate.test.ts,覆盖了三大行为:
- 默认输出 TypeScript 迁移:不传任何选项时,生成的
.ts文件内容与预期的resultsTemplates模板完全一致(通过 sinon stub 掉文件写入与 DataSource 加载来断言内容); outputJs输出 JavaScript:传入outputJs: true时生成.js文件,内容匹配 JavaScript 模板;- 自定义时间戳:传入固定 timestamp 后,文件名变为
{timestamp}-test-migration.ts。
值得注意,该测试的 enabledDrivers 覆盖了 postgres、mssql、mysql、mariadb、better-sqlite3、oracle、cockroachdb 等主流数据库(migration-generate.test.ts),说明「实体 vs 数据库差值 → 生成迁移 SQL」的核心路径在不同方言上都被持续验证着。这也提醒我们:虽然本文示例统一用了 ALTER TABLE ... RENAME 的写法,但实际 SQL 必须由你连接的数据库驱动来决定。
小结
TypeORM 的 migration:generate 把「读实体、连数据库、算差值、写迁移」四步压缩成一条命令。核心要记住四点:
- 参数:
-d必须指向导出了 DataSource 实例的文件,迁移名可以只写名字也可以带目录路径; - 产物:默认生成
{TIMESTAMP}-{name}.ts,含up/down及name属性,全部 SQL 由createSchemaBuilder().log()自动对比产出; - 纯 JS 项目:
-o输出.js(CommonJS),叠加--esm切换为 ESM; - 工程化用法:
--pretty提升可读性、--dryrun先预览、--check守护 CI、无差异时以退出码1明确拒绝生成,并引导使用migration:create。
关于迁移的完整工作流(手动创建、执行、回滚、状态查看、假执行与 API 说明),可以按顺序阅读 01-why、02-setup、03-creating、05-executing、06-reverting、07-status 与 09-api,把自动生成真正融入你的数据库版本管理流程。
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