TypeORM 迁移(Migrations)实战指南:生产环境下安全地同步数据库结构变更
2026/9/10 10:46:44 网站建设 项目流程

TypeORM 迁移(Migrations)实战指南:生产环境下安全地同步数据库结构变更

【免费下载链接】typeormTypeScript & JavaScript ORM for Node.js — supports PostgreSQL, MySQL, MariaDB, SQLite, SQL Server, Oracle, and more.项目地址: https://gitcode.com/GitHub_Trending/ty/typeorm

在开发环境,TypeORM 可以让实体自动同步到数据库;但当项目上线、数据库中积累了真实数据后,这种“一把梭”的自动同步将变得极度危险。本文将以当前仓库 typeorm 的官方迁移文档(docs/docs/migrations/01-why.md)为主线,讲解为什么生产环境必须使用 Migration(迁移)、一个迁移文件究竟由什么构成、如何用一条 SQL 完成一次典型的“改列名”上线,并结合仓库源码拆解 TypeORM 迁移的底层工作方式。读完本文,你将掌握从“何时需要迁移”到“如何写出第一个可上线的迁移”的完整思路,并能据此设计自己的数据库版本演进方案。

为什么一旦上线,synchronize: true就不再安全?

TypeORM 提供了synchronize选项,它会在每次应用启动时,根据实体元数据自动把数据库 Schema 同步成最新状态。在开发期这个特性非常方便——改完实体、重启服务,表结构就跟着变了。

但一旦进入生产环境,情况就完全不同了。官方文档(docs/docs/migrations/01-why.md)给出的判断非常明确:

Typically, it is unsafe to usesynchronize: truefor schema synchronization on production once you get data in your database.

也就是说:当数据库中开始有真实数据后,用synchronize: true做 Schema 同步通常是不安全的。原因很直观:

  • 自动同步会基于实体“推断”出需要执行的 DDL,你无法控制它何时、以何种顺序执行,也无法在变更前先做数据备份、数据清洗或平滑迁移;
  • 一次粗心的同步可能直接触发DROP COLUMN、重建表等破坏性操作,导致线上数据丢失;
  • 同步是“隐式”发生的,运维与后续开发者难以追溯某次结构变更究竟是谁、在哪个版本、以什么 SQL 触发的,审计与回滚无从谈起。

从源码实现可以更清楚地看到synchronize的行为方式。在 DataSource.ts 中,建立连接时会判断this.options.synchronize是否为真,若为真则自动调用this.synchronize(),直接基于实体与数据库当前结构的比对结果执行同步。它背后走的是 RdbmsSchemaBuilder 那套“全量比对、全量修补”的逻辑,对开发库友好,但对生产库是一场豪赌。

这正是 Migration 登场的时机——它是官方推荐、面向生产环境的 Schema 变更管理手段。

什么是 Migration:一个携带 SQL 的文件

迁移的定义并不神秘。文档中的原文定义是:

A migration is just a single file with SQL queries to update a database schema and apply new changes to an existing database.

即:迁移就是一个携带 SQL 查询的文件,用来更新数据库 Schema,把新的结构变更应用到已有的数据库上。它把“数据库结构演进”这件事从 ORM 的自动推断,转变成开发者手写、可审查、可版本化、可追溯的显式脚本。

在 TypeORM 中,一个迁移文件对应一个类,这个类必须实现MigrationInterface。我们可以直接在源码中查看该接口的完整契约(src/migration/MigrationInterface.ts):

export interface MigrationInterface { /** * Optional migration name, defaults to class name. */ name?: string /** * Optional flag to determine whether to run the migration in a transaction or not. */ transaction?: boolean /** * Run the migrations. */ up(queryRunner: QueryRunner): Promise<any> /** * Reverse the migrations. */ down(queryRunner: QueryRunner): Promise<any> }

也就是说,一个迁移类需要实现两个方法:

方法作用
up(queryRunner)执行迁移:把数据库从当前版本升级到新版本,写“前进”的 SQL
down(queryRunner)回滚迁移:撤销up所做的更改,写“后退”的 SQL

两个方法都能拿到一个QueryRunner对象,所有数据库操作都通过它来执行。

与此同时,src/migration/Migration.ts 中的Migration类描述了迁移在数据库中的“档案记录”,包含id(执行顺序)、timestamp(时间戳,用于排序)、name(类名)、instance(迁移实例)与transaction(是否在事务中执行)等字段。可见 TypeORM 对迁移的管理是“元数据 + 文件 + 数据库记录”三者结合的体系,这一点会在后文继续展开。

第一个迁移案例:给已有生产库的列改名

纸上得来终觉浅,文档用一个非常贴切的实战场景说明了迁移的完整价值:你有一个已经运行数月的生产数据库与对应的Post实体:

import { Entity, Column, PrimaryGeneratedColumn } from "typeorm" @Entity() export class Post { @PrimaryGeneratedColumn() id: number @Column() title: string @Column() text: string }

这张post表在线上稳定运行,里面存着成千上万条帖子。现在,业务方要求发布一个新版本,把title这一列改名为name。请问你要怎么做?

  • 如果直接改实体并打开synchronize,TypeORM 有可能通过删表重建或风险不可控的方式完成任务,在真实数据面前无异于“裸奔”;
  • 正确的做法是:写一个迁移文件,用一条标准的 DDL 完成列的重命名。以 PostgreSQL 方言为例:
ALTER TABLE "post" RENAME COLUMN "title" TO "name";

执行这条 SQL 之后,数据库 Schema 就已经准备好与你的新代码配合了。TypeORM 提供的 “migrations” 机制,正是让你有这样一个受控的“容器”,把这类 SQL 写下来,并在你需要的时候(发布流程中)可靠地执行。

对应的迁移文件整体形态如下:

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"`, ) // 撤销 up 方法中做的修改 } }

注意两个要点:

  1. updown必须是互逆的:uptitle改名成namedown就必须把name改回title。这样才能保证迁移可回滚;
  2. down用于撤销最近一次迁移,是灾难恢复与版本回退的保险丝。

从哪来、去哪存:手动创建迁移文件

在真实项目中,迁移文件通常由 TypeORM CLI 生成骨架,你再填充具体的 SQL 逻辑。官方文档(docs/docs/migrations/03-creating.md)给出了手动创建的方式:

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}是生成时刻的时间戳。之所以用时间戳作为文件名前缀,是因为 TypeORM 需要依据时间戳决定迁移的执行顺序(参见前文 Migration.ts 中的timestamp字段)。

打开这个文件,你会看到 MigrationCreateCommand 为你生成的骨架——一个实现了up/down的空迁移类,等待你填入迁移逻辑。

可见,迁移的本质是“时间戳命名 + 成对的前进/回滚方法”,这让每次结构变更都成为一个独立的、可排序的、可回滚的发布单元。

交给 DataSource:迁移工作的“总开关”

写好迁移文件后,还需要在 DataSource 中把它“挂载”到 TypeORM 的运行体系里。官方迁移系列的配置文档(docs/docs/migrations/02-setup.md)给出了标准配置模板:

export default new DataSource({ // 基础设置 synchronize: false, migrations: [__dirname + "/migrations/**/*{.js,.ts}"], // 可选设置 migrationsRun: false, migrationsTableName: "migrations", migrationsTransactionMode: "all", // 其他选项…… })

下面逐一说明这些选项的含义,并补充源码中的取值细节。

synchronize:必须关闭

正如本文第一节强调的,想用迁移管理结构演进,第一步就是关闭自动同步,否则迁移会失去意义。在 BaseDataSourceOptions.ts 中,该选项的注释也明确警告:不要在生产环境使用它,否则可能丢失生产数据。

migrations:告诉 TypeORM 去哪里找迁移

该选项接受“迁移类”和“迁移文件目录”两种形式(源码注释见 BaseDataSourceOptions.ts)。

最省心的方式是传入目录 + glob 通配符,让 TypeORM 自动加载目录下的全部迁移文件:

migrations: [__dirname + "/migrations/**/*{.js,.ts}"]

同时声明.js.ts两种扩展名有一个实际好处:开发环境可以直接跑 TypeScript 源码,而生产环境(例如打包进 Docker 镜像时)可以运行编译后的 JavaScript,两者共用同一套配置。

如果你希望获得更精细的控制,也可以显式导入具体的迁移类:

import FirstMigration from "./migrations/TIMESTAMP-first-migration" import SecondMigration from "./migrations/TIMESTAMP-second-migration" export default new DataSource({ migrations: [FirstMigration, SecondMigration], })

代价是每次新增迁移都要手动改这段代码,文档提示这种方式“需要更多手工操作,也更容易出错”,因此日常开发推荐 glob 目录方式。

migrationsRun:是否随应用启动自动执行

如果设为true,每次应用启动时 TypeORM 都会自动执行尚未跑过的迁移;默认值为false(源码见 BaseDataSourceOptions.ts)。在需要“启动即就绪”的部署形态(如无单独迁移步骤的 PaaS 平台)下很实用;否则通常交给 CI/CD 或 CLI 显式执行。

migrationsTableName:迁移记录表的名字

TypeORM 会把已经执行过的迁移登记在一张专门表里(默认表名就是migrations)。你可以按需改名,例如:

migrationsTableName: "some_custom_migrations_table"

migrationsTransactionMode:迁移的事务模式

该选项控制迁移执行时的事务边界,可选值见 BaseDataSourceOptions.ts,含义如下:

取值行为
all(默认)把一次要执行的所有迁移包进单个事务
none不使用事务执行迁移
each每条迁移各自运行在独立事务中

MigrationInterface中每个迁移类还可以通过自身的transaction属性覆盖全局行为(仅当全局模式为eachnone时可覆盖),这为特殊场景(比如某条 DDL 在事务外执行更稳妥)留出了弹性空间。

手写太累?让 TypeORM 帮你生成迁移

需要强调一点:像“改列名”这样的迁移,很多时候你根本不需要手写 SQL。TypeORM 提供了自动生成能力:它会把你实体中做的修改与服务器上现有的数据库 Schema 做比对,然后自动产出迁移文件并写清所需的全部 SQL。官方文档(docs/docs/migrations/04-generating.md)中展示了核心命令:

typeorm migration:generate -d <path/to/datasource> <migration-name>

-d指定了 DataSource 实例定义所在的路径。假如你刚把Post实体的title属性改名成name,执行生成命令后,TypeORM 会产出{TIMESTAMP}-post-refactoring.ts,其up中自动包含类似下面的 SQL:

ALTER TABLE "post" ALTER COLUMN "title" RENAME TO "name"

down自动写出方向相反的重命名语句,形成可回滚的对称结构。若检测不到任何 Schema 变化,命令会以退出码1结束,提示你无需生成。文档给出的经验法则是:每次修改模型之后都生成一次迁移,让结构变更始终以迁移文件的形态沉淀下来。可见,“迁移 = 手写 + 自动生成”双轨并行,前者适合复杂的数据修补,后者适合常规的模型演进。

结合源码理解迁移的整体工作流

把以上内容串起来,TypeORM 的迁移机制在源码层面可以归纳为一条清晰的链路:

  1. 加载:通过migrations选项(glob 或类数组)在 DataSource 初始化时加载迁移类;
  2. 实例化与排序:每个迁移被包装为带idtimestampnameinstance的 Migration 记录,按时间戳决定执行顺序;
  3. 登记:已执行的迁移会被写入migrationsTableName指定的记录表(默认migrations),下次执行时跳过已完成项;
  4. 执行与回滚:运行迁移时调用迁移实例的up(queryRunner),回滚时调用down(queryRunner)up/down中的一切 SQL 都经由QueryRunner提交给数据库(接口定义见 MigrationInterface.ts);
  5. 事务保障:由migrationsTransactionModeall/none/each)与单个迁移的transaction标志共同决定事务边界,保证迁移要么全部生效、要么按预期粒度可控回滚。

这也回答了最开始的疑问:为什么说 Migration 是生产环境结构变更的标准答案——因为对比synchronize的“自动、隐式、不可审计”,迁移做到了“显式、可审查、可排序、可回滚、可记录”。

小结

  • 生产库存在真实数据后,应关闭synchronize: true,改用 Migration 管理 Schema 演进;
  • 一个迁移 = 一个携带 SQL 的文件 = 实现up/down两个方法的类,前者前进、后者回滚,二者互逆;
  • 以“改列名”为代表的任何结构变更,都可以写成一条(或一组)可执行的 DDL,交给 TypeORM 的迁移机制在发布时受控执行;
  • 迁移文件用npx typeorm migration:create手工起步、用typeorm migration:generate根据实体差异自动生成;
  • 在 DataSource 中正确配置synchronizemigrationsmigrationsRunmigrationsTableNamemigrationsTransactionMode,迁移体系即告就绪。

本文所在的官方文档还包含更完整的迁移专题章节:迁移的执行(05-executing.md)、回滚(06-reverting.md)、查看状态(07-status.md)、伪造迁移(08-faking.md)与完整 API(09-api.md),读者可以按需继续深入。

【免费下载链接】typeormTypeScript & JavaScript ORM for Node.js — supports PostgreSQL, MySQL, MariaDB, SQLite, SQL Server, Oracle, and more.项目地址: https://gitcode.com/GitHub_Trending/ty/typeorm

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询