NestJS博客实战09-数据迁移Migrations介绍

1,369 阅读10分钟
by 雪隐 from https://juejin.cn/user/1433418895994094
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权

上一章讲了,怎么在NestJS使用typeORM,这一章接着上一章的内容,继续来讨论2个点,一个是数据迁移。另外一个是导入所有的表结构。

通过TypeORM的forRoot/forRootAsync方法里面synchronize参数设置为true就能很轻松的把我们的表结构同步到数据库里面。没有老的博客项目,为什么需要数据迁移呢?

  1. synchronize设置为true一般只在开发/测试环境中使用。如果在生产环境中使用,会导致数据的丢失。
  2. 一个项目上线以后难免会发生一些需求的变更。当一些需求影响到数据库的时候。Migrations的迁移的用处就非常大了。

Migrations#

Migrations提供了一种增量更新数据库模式的方法,使其与应用程序的数据模型保持同步,同时保留数据库中的现有数据。为了生成、运行和恢复迁移,TypeORM提供了一个专用的CLI。

迁移类与Nest应用程序源代码是分开的。它们的生命周期由TypeORM CLI维护。因此,您无法在迁移中利用依赖项注入和其他Nest特定功能。要了解有关迁移的更多信息,请按照TypeORM文档中的指南进行操作。

一旦进入生产阶段,就需要将模型更改同步到数据库中。通常,在数据库中获得数据后,在生产上使用synchronize:true进行模式同步是不安全的。这就是迁移的作用所在。

迁移只是一个带有sql查询的文件,用于更新数据库模式并将新的更改应用于现有数据库。

假设您已经有了一个数据库和一个post实体:

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

你的实体在生产中工作了几个月,没有任何变化。您的数据库中有数千条数据。

现在,您需要制作一个新版本,并将title重命名为name。你会怎么做?

您需要使用以下SQL查询(postgres方言)创建一个新的迁移:

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

一旦您运行了这个SQL查询,您的数据库模式就可以使用新的代码库了。TypeORM提供了一个可以编写此类sql查询并在需要时运行它们的地方。这个地方被称为“migrations”。

创建新的迁移

在创建新迁移之前,您需要正确设置数据源选项:

{
    type: "mysql",
    host: "localhost",
    port: 3306,
    username: "test",
    password: "test",
    database: "test",
    entities: [/*...*/],
    migrations: [/*...*/],
    migrationsTableName: "custom_migration_table",
}

在这里我们设置了两个选项:

  • “migrationsTableName”:“migrations”-仅当您需要迁移表名称与“migration”不同时,才指定此选项。
  • “migrations”:[/*…*/]-需要由TypeORM加载的迁移列表

一旦设置了连接选项,就可以使用CLI创建新的迁移:

typeorm migration:create ./path-to-migrations-dir/PostRefactoring

这里,PostRefactoring是迁移的名称,您可以指定任何您想要的名称。运行该命令后,您可以看到在名为{TIMESTAMP}-PostRefactoring.ts的“migration”目录中生成的新文件,其中{TIMESTAMP}是生成迁移时的当前时间戳。现在,您可以打开该文件并在其中添加迁移sql查询。

您应该在迁移中看到以下内容:

import { MigrationInterface, QueryRunner } from "typeorm"
export class PostRefactoringTIMESTAMP implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {}
async down(queryRunner: QueryRunner): Promise<void> {}
}

您必须使用迁移代码填充两种方法:向上up和向下down。up必须包含执行迁移所需的代码。无论上行发生什么变化,down都必须恢复。down方法用于恢复上一次迁移。

在向上和向下的内部都有一个QueryRunner对象。所有数据库操作都是使用此对象执行的。了解有关查询运行器的更多信息。

让我们看看Post更改后的迁移情况:

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事件导致的变化 }
}

运行和恢复迁移

在生产环境中运行迁移,可以使用CLI命令运行迁移:

typeorm migration:run -- -d path-to-datasource-config

typeorm migration:createtypeorm migrate:generate将创建.ts文件,除非您使用o标志(请参阅生成迁移中的更多内容)。migration:runmigration:revert命令仅适用于.js文件。因此,在运行命令之前需要编译typescript文件。或者,您可以将ts-nodetypeorm结合使用来运行.ts迁移文件。

ts-node示例:

npx typeorm-ts-node-commonjs migration:run -- -d path-to-datasource-config

ESM项目中ts-node的示例:

npx typeorm-ts-node-esm migration:run -- -d path-to-datasource-config
npx typeorm-ts-node-esm migration:generate ./src/migrations/update-post-table -d ./src/data-source.ts

此命令将执行所有挂起的迁移,并按时间戳顺序运行它们。这意味着所有在创建的迁移的up方法中编写的sql查询都将被执行。这就是全部!现在,您的数据库架构是最新的。

如果出于某种原因想要恢复更改,可以运行:

typeorm migration:revert -- -d path-to-datasource-config

此命令将在最近执行的迁移中down执行。如果需要恢复多次迁移,则必须多次调用此命令。

伪造迁移和回滚

您也可以使用--fake标志(简写-f)伪造运行迁移。这将在不运行迁移的情况下将迁移添加到迁移表中。这对于在对数据库进行手动更改后创建的迁移或在外部运行迁移(例如通过其他工具或应用程序)时创建的迁移非常有用,并且您仍然希望保持一致的迁移历史记录。

typeorm migration:run --fake

这在回滚时也是可能的。

typeorm migration:revert --fake

Transaction模式

默认情况下,TypeORM将在单个包装事务中运行所有迁移。这对应于--transaction all标志。如果需要更细粒度的事务控制,可以使用--transaction each标志单独包装每个迁移,或者使用--transaction none标志选择不将迁移全部包装在事务中。

除了这些标志之外,您还可以通过将MigrationInterface上的事务transaction属性设置为truefalse来覆盖每次迁移的事务行为。这只适用于每个each事务或无none事务模式。

import { MigrationInterface, QueryRunner } from "typeorm"
export class AddIndexTIMESTAMP implements MigrationInterface {
    transaction = false
    async up(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(
            `CREATE INDEX CONCURRENTLY post_names_idx ON post(name)` )
    }
    async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(
            `DROP INDEX CONCURRENTLY post_names_idx`, )
    }
}

生成迁移

TypeORM能够根据您所做的架构更改自动生成迁移文件。 假设您有一个带有标题列的Post实体,并且您已经将名称title更改为name。您可以运行以下命令:

typeorm migration:generate PostRefactoring -d path-to-datasource-config

如果您遇到任何错误,它要求您有迁移名称和数据源的路径。你可以试试这个选项

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

它将生成一个名为{TIMESTAMP}-PostRefactoring.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"`,
        )
    }
}

或者,您也可以使用o--outputJs的别名)标志将迁移输出为Javascript文件。这对于未安装TypeScript附加包的仅限Javascript的项目非常有用。此命令将生成一个新的迁移文件{TIMESTAMP}-PostRefactoring.js,其中包含以下内容:

const { MigrationInterface, QueryRunner } = require("typeorm")
module.exports = class PostRefactoringTIMESTAMP {
    async up(queryRunner) {
        await queryRunner.query(
            `ALTER TABLE "post" ALTER COLUMN "title" RENAME TO "name"`,
        )
    }
    async down(queryRunner) {
        await queryRunner.query(
            `ALTER TABLE "post" ALTER COLUMN "name" RENAME TO "title"`,
        )
    }
}

看,您不需要自己编写查询。生成迁移的经验法则是在对模型进行每次更改后生成迁移。要将多行格式应用于生成的迁移查询,请使用p--pretty的别名)标志。

DateSource选项

如果需要运行/revert/generate/show迁移,请使用-d--dataSource的别名)并将dataSource实例定义为参数的文件的路径传递给

typeorm -d <your-data-source-path> migration:{run|revert}

时间戳选项

如果需要为迁移名称指定时间戳,请使用-t--timestamp的别名)并传递时间戳(应该是非负数)

typeorm -t <specific-timestamp> migration:{create|generate}

您可以从以下位置获取时间戳:

Date.now() /* OR */ new Date().getTime()

使用迁移API编写迁移

为了使用API来更改数据库架构,您可以使用QueryRunner。详细的QueryRunner的各种方法,这里就不多做介绍了。大家自己参照官网学习。

例子:

import {
  MigrationInterface,
  QueryRunner,
  Table,
  TableIndex,
  TableColumn,
  TableForeignKey,
} from 'typeorm';
export class QuestionRefactoringTIMESTAMP implements MigrationInterface {
  async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.createTable(
      new Table({
        name: 'question',
        columns: [
          { name: 'id', type: 'int', isPrimary: true },
          { name: 'name', type: 'varchar' },
        ],
      }),
      true,
    );
    await queryRunner.createIndex(
      'question',
      new TableIndex({ name: 'IDX_QUESTION_NAME', columnNames: ['name'] }),
    );
    await queryRunner.createTable(
      new Table({
        name: 'answer',
        columns: [
          { name: 'id', type: 'int', isPrimary: true },
          { name: 'name', type: 'varchar' },
          { name: 'created_at', type: 'timestamp', default: 'now()' },
        ],
      }),
      true,
    );
    await queryRunner.addColumn(
      'answer',
      new TableColumn({ name: 'questionId', type: 'int' }),
    );
    await queryRunner.createForeignKey(
      'answer',
      new TableForeignKey({
        columnNames: ['questionId'],
        referencedColumnNames: ['id'],
        referencedTableName: 'question',
        onDelete: 'CASCADE',
      }),
    );
  }
  async down(queryRunner: QueryRunner): Promise<void> {
    const table = await queryRunner.getTable('answer');
    const foreignKey = table.foreignKeys.find(
      (fk) => fk.columnNames.indexOf('questionId') !== -1,
    );
    await queryRunner.dropForeignKey('answer', foreignKey);
    await queryRunner.dropColumn('answer', 'questionId');
    await queryRunner.dropTable('answer');
    await queryRunner.dropIndex('question', 'IDX_QUESTION_NAME');
    await queryRunner.dropTable('question');
  }
}

在项目中配置Migrations

打开package.jsonscripts中加入下面的命令:

  "scripts": {
    "typeorm": "typeorm-ts-node-commonjs -d ./src/common/database/ormconfig.ts",
    "migration:generate": "f() { npm run typeorm migration:generate -p \"./src/migrations/$@\"; }; f",
    "migration:create": "typeorm-ts-node-commonjs migration:create",
    "migration:run": "npm run typeorm migration:run",
    "migration:revert": "npm run typeorm migration:revert",
  },

这里所有的命令的用法在上文都有提到过,就不多做解释了。

打开/src/common/database/ormconfig.ts文件,编写配置内容。参照本章创建新的迁移的内容,写出DataSource并且export出来。因为migrations迁移类与Nest应用程序源代码是分开的。它们的生命周期由TypeORM CLI维护,所以无法取到ConfigService对象,因此直接调用config/configuration里的getConfig方法。

import { DataSource, DataSourceOptions } from 'typeorm';
import { getConfig } from '../../config/configuration';

export function buildConnectionOptionsForMigrations() {
  const { MYSQL_CONFIG: config } = getConfig();
  const logFlag = config['LOG_ON'] === 'true';

  const entitiesDir =
    process.env.NODE_ENV === 'test'
      ? [__dirname + `/**/*.${config.type}.entity.ts`]
      : [__dirname + `/**/*.${config.type}.entity{.js,.ts}`];

  return {
    ...config,
    entities: entitiesDir,
    logging: logFlag,
  } as TypeOrmModuleOptions;
}

export const connectionParams = buildConnectionOptionsForMigrations();

export default new DataSource({
  ...connectionParams,
  migrations: ['src/common/database/migrations/**'],
  subscribers: [],
} as DataSourceOptions);

试运行

  1. create
npm run migration:create ./src/common/database/migrations/test1

# 运行结果
#> day08@0.0.1 migration:create
#> typeorm-ts-node-commonjs migration:create ./src/common/database/migrations/test1

#Migration /Users/mac/Documents/workspace/nodejs/juejin-shenblog/day09/src/common/database/migrations/1682303306379-test1.ts has been generated successfully.

生成的1682303306379-test1.ts文件

import { MigrationInterface, QueryRunner } from "typeorm"

export class Test11682303306379 implements MigrationInterface {

    public async up(queryRunner: QueryRunner): Promise<void> {
    }

    public async down(queryRunner: QueryRunner): Promise<void> {
    }

}
  1. run 修改刚刚生成的文件,创建一张表试试。
import { MigrationInterface, QueryRunner } from 'typeorm';

export class Test11682303306379 implements MigrationInterface {
  name = 'Test11682303306379';
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`DROP TABLE IF EXISTS \`article\`;`);
    await queryRunner.query(
      `CREATE TABLE \`article\` (\`id\` int(11) NOT NULL AUTO_INCREMENT,\`title\` varchar(100) DEFAULT NULL,\`body\` longtext,\`tag\` varchar(50) DEFAULT NULL COMMENT '每条记录的标签',\`category\` varchar(40) DEFAULT NULL,\`created_at\` date DEFAULT NULL,\`updated_at\` date DEFAULT NULL,\`status\` tinyint(1) DEFAULT '1' COMMENT '1表示正常  0表示删除 ',\`type\` tinyint(3) DEFAULT '1' COMMENT '1:原创; 0:转载',\`views\` int(11) DEFAULT '0',\`markdown\` tinyint(1) DEFAULT NULL,PRIMARY KEY (\`id\`)) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8mb4;`,
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      DROP TABLE IF EXISTS \`article\`;`);
  }
}

然后运行下面的命令,可以看到表正常被创建就没问题了

npm run migration:run

并且在数据库里面会创建一个migrations表,并且添加了一条记录

idtimestampname
11682303306379Test11682303306379
  1. revert
npm run migration:revert

查看数据库可以看到migrations表的数据被清空,并且,article表也不存在了。其他的都不多做介绍了。

导入表

migrations文件夹中放了一个init.sql文件,大家可以试着把里面的表都导入到数据库,并且自己写实体类entities。重复的工作我就不细说了。如果您觉得这篇文章对您有帮助别忘了点赞/评论。谢谢大家🙏!

本章代码

代码