前言
我们开发的项目上线生产环境,如果需要将新模型更改同步到数据库中,通常在数据库中获取数据后,使用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 是回滚代码,往后面再讲解迁移代码
迁移相关指令以及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语句
先吃透吧,那么将会有不少的提升,无论是写业务、写迁移、优化,都会有不少帮助
此外,这里涉及到的只是后端技术的九牛一毛,还需要更多的沉淀