nestjs-typeorm迁移

418 阅读7分钟

前言

我们开发的项目上线生产环境,如果需要将新模型更改同步到数据库中,通常在数据库中获取数据后,使用synchronize:true,但这在生产模式同步是极其不安全的(常见的案例是改个字段都会出现先删后添加,很不智能),并且即使平时同步操作没问题,生产环境也不可能这么直接同步,不可预见问题是个未知数

并且,我们前面开发过程中也会关闭 synchronize,毕竟在开发也会给我们带来不必要的麻烦,我们前面还引入了 typeorm-config.ts 手动拉取队友代码后同步,就是怕覆盖或者出问题,线上更得注意了

因此这时候使用迁移,可以解决此类问题,迁移只是一个带有 SQL 语句的文件,用于更新数据库架构并将新更改应用于现有数据库

并且,这边也简单回顾一下 typeorm 模型同步操作,相比与之前有一点改进(之前的文章也额外更新了一下)

nestjs-关系数据库(mysql)模型、关系、操作typeorm迁移参考

typeorm 手动同步数据库回顾

自己前面讲的mysql 和 typeorm那个手动同步操作,发现每次都要自己引入 entities,并且还要设置相对路径,很麻烦

这里,我们可以直接通过设置目录的方式来引入(自己测试过没有问题,有问题可以切回纯手动版本),不仅如此,这里还无需使用相对路径(也是看了迁移的文档有感,但是迁移的怎么尝试不行哈,得手动😂)

下面是我们的 typeorm-config.ts 其和我们的 main.ts 一个目录

export const TypeormConfig: TypeOrmModuleOptions = {
    type: 'mysql',
    host: ...,
    port: ...,
    username: ...,
    password: ...,
    database: ...,
    autoLoadEntities: true, //自动查找entity实体,手动的需要手动导入,自动的适合设置 
    synchronize:false, //手动记得设置为false
    //为什么是这个目录,我呢的 typeorm-config.ts 在src 目录下第一级
    //为什么是js,具体不太了解,ts不行,因此那就是编译后的 js 文件了,其目录就是如此,官方案例基本也是js
    //因此同步之前需要build一下
    entities: ['./**/entities/*.entity.js'],
};
//这个是给脚本用的,脚本会根据这个参数来更新一次
export const AppDataSource: any = new DataSource(TypeormConfig as DataSourceOptions);

设置脚本,由于其使用build后的代码,才能使用目录功能(这里ts不识别报错),因此需要先 build,所以可以直接命令设置到一起

"scripts": {
    ...
    "db:sync": "nest build && typeorm-ts-node-commonjs -d ./src/typeorm-config.ts schema:sync"
},

typeorm 迁移

比如我们有这个一个 entity,我们可能需要将 title 字段改为 name,或者新增一个字段 text,直接同步则会有数据丢失风险或者直接同步失败,因此我们需要使用到迁移

@Entity()  
export class Post {  
    @PrimaryGeneratedColumn()  
    id: number;  

    @Column()  
    title: string;  

    @Column()  
    text: string;  
}

创建迁移文件

首先生成我们的迁移文件,调用下面命令后

其会在 /src/migration 目录下,生成一个 {时间戳}-article.ts 文件,其中 article 是我们自己起的名字,时间戳是自己生成的

//前面是创建命令, create 后面是目录
typeorm-ts-node-commonjs migration:create ./src/migration/article

比如我分别起名 v1、v2(也就是前面的 article),自动生成两个文件,文件中的 up 是迁移代码,down 是回滚代码,往后面再讲解迁移代码

QQ_1723533340887.png

迁移相关指令以及typeorm配置

下面的目录中 ./src/migration/article 不多说,有一个 ./src/typeorm-config.ts我们应该属性,这个就是需要我们配置 typeorm 才能生效了,因此我们后面需要配置

//创建迁移文件指令
typeorm-ts-node-commonjs migration:create ./src/migration/article

//执行迁移指令
typeorm-ts-node-commonjs migration:run -d ./src/typeorm-config.ts

//执行回滚指令(有问题执行回滚)
typeorm-ts-node-commonjs migration:revert -d ./src/typeorm-config.ts

//根据现有的entitys生成迁移指令和文件到指定文件夹,跟创建类似,只不过有迁移代码
//需要注意的是,这里的迁移代码,基本和直接同步存在的问题一样,直接执行数据丢失时家常便饭
//因此,仅供参考,有些迁移代码还是可以使用的
typeorm-ts-node-commonjs migration:generate ./src/migration/article -d ./src/typeorm-config.ts

typeorm-config.ts文件配置一下 typeorm 迁移

export const TypeormConfig: TypeOrmModuleOptions = {
    ...
    //其他的保持一样,这里需要引入我们生成的前期文件类名
    //具体为何不使用目录,不是我不使用,根据文档 ts、js都尝试了,无效
    //实际上迁移代码也不经常搞,不多的话写到一个版本文件,手动导一下应该也没事(主要是目录不行哈😂)
    //有尝试性的可以回复我,多谢哈
    migrations: [V11723512030883],
};

到这里迁移的就配置完了,只需要执行迁移指令就可以完成迁移了

迁移指令怎么写呢,下面简单讲一下,过于复杂的,我也费劲哈,一般的还行哈,我还在学习中(不能一心投入也是痛哈,毕竟不是专门做这个的,现在情况,一不小心就会失业,只能有时间学习尝试并写一下哈,其他的也有不少要学习研究,难受乎😂)

迁移代码

我们就以迁移方法里面代码为例

喜欢写 sql指令的,直接使用 queryRunner.query 方法,方便的很

对于 sql指令还不够熟练地,直接使用 typepeorm 提供的现成即可,参考

//将我们的 article 表中的 content 改为 text
public async up(queryRunner: QueryRunner): Promise<void> {
    //直接使用 query 直接编写 sql指令
    await queryRunner.query(
        `ALTER TABLE article RENAME COLUMN content TO text`,
    );
    //同样的操作,使用typeorm提供的现成的方法,但是执行起来会发现,多了很多东西,整体是无误的
    await queryRunner.renameColumn('article', 'content', 'text')
}

ps:那个 down 代码是回退功能的,改名过去,回退则是改回来,创建则对应删除,就不多写了

文档一个很好的案例,如果不会用的,可以直接参考下面,避免参数不会用,也可以点进去文档查看一下哈

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',
                    },
                ],
            }),
            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('question');
        const foreignKey = table.foreignKeys.find(
            (fk) => fk.columnNames.indexOf('questionId') !== -1,
        );
        await queryRunner.dropForeignKey('question', foreignKey);
        await queryRunner.dropColumn('question', 'questionId');
        await queryRunner.dropTable('answer');
        await queryRunner.dropIndex('question', 'IDX_QUESTION_NAME');
        await queryRunner.dropTable('question');
    }
}

scripts调整

上面的一些发现那迁移指令用着很不方便,我们直接将其放到 package.json 的 scripts 中,并且稍微改进一下,使用方便通用一些

此外,我们加入 config,方便几个目录能够动态调整

scripts 中 通过 $npm_package_config_{config参数名}获取 config 中统一配置的参数

"scripts": {
    "build": "nest build",
    ...
    //需要先编译,否则那个动态加载 entity 会出问题
    "db:sync": "nest build && typeorm-ts-node-commonjs -d ./src/typeorm-config.ts schema:sync",
    //创建操作,我们的目录部分使用config的参数
    "migrate:create": "typeorm-ts-node-commonjs migration:create ./src/migration/$npm_package_config_migrateDir",
    //迁移,执行迁移钱我们编译一下,以防万一
    "migrate": "nest build && typeorm-ts-node-commonjs migration:run -d ./src/typeorm-config.ts",
    //回退代码
    "migrate:revert": "typeorm-ts-node-commonjs migration:revert -d ./src/typeorm-config.ts",
    //动态生成迁移代码,仅供参考
    "migrate:generate": "nest build && typeorm-ts-node-commonjs migration:generate ./src/migration/$npm_package_config_migrateDir -d ./src/typeorm-config.ts"
},

//加入config,方便其他指令都使用同一个目录参数,当然不用也是可以的
"config": {
    "migrateDir": "v2"
}

最后

了解迁移能帮助我们解决一些生产环境问题,要是想做的更复杂,那么吧 sql语句 先吃透吧,那么将会有不少的提升,无论是写业务、写迁移、优化,都会有不少帮助

此外,这里涉及到的只是后端技术的九牛一毛,还需要更多的沉淀