背景
先说下背景,在使用electron开发一个桌面IM应用,版本迭代过程中数据库一直都是每次更新重新同步一次,具体到我使用的typeORM框架就是设置:
synchronize:true
一般情况下,当版本涉及到数据库更新,需要通过sql语句先更新数据库。本文主要是介绍下在应用更新过程中数据库同步遇到的问题,以及我后续是如何规范更新流程的;
synchronize
一开始使用'synchronize:true'其实在QA测试过程中没遇到过什么问题.虽然有考虑过更加规范的数据库更新流程,由于优先级不高,一直没时间去做。但后来把错误上报后统计,发现每周都会有几十个报错:
QueryFailedError: SQLITE_ERROR: table "" already exists
为什么没有用户反馈呢?
查了下相关的资料issueissue,主要两个原因导致:
1、entity(表)命名有大写字母;
2、每次都同步数据库(synchronize:true,不建议);
检查跟第一个原因无关。那就是第二个原因了,关键是这个问题无法重现,或者无法稳定重现。但可以理解为什么没有用户反馈,两个原因:
1、出现这个报错的原因是第二次或第二次以上同步才会出现,而我们数据库第一次同步时已更新了表,所以即使版本迭代过程中数据库有修改也不影响用户使用;
2、这个错误在出现时已被try...catch捕获(错误日志都是在这一层上报的),不会中断程序继续执行(可参考我关于错误捕获的文章);
为了解决这个问题,也顺便规范版本迭代过程中的数据库更新,我先后尝试了两种方案:
1、typeorm本身的迁移方案;
2、自己写的迁移方案;
后面介绍为什么不用typeorm的迁移方案;
typeorm migration
typeORM migration方案的流程如下:
1、修改typeorm配置,
"migrationsTableName": "migrations" // 一般不用设置,只是自己数据库中有表名和这个名字冲突时才设置,这个表会自动生成,是用来记录执行过哪些迁移脚本的(执行过的迁移脚本会生成一条记录,后面不再执行)
"migrations": ["migration/*.js"] // 指定加载迁移脚本的目录
"cli": { "migrationsDir": "migration" } // 指定迁移脚本的生成目录,但我是动态的配置文件,并不在根目录生成,所以不需要
2、生成迁移脚本;
npx typeorm migration:create -n PostRefactoring // 使用当前目录安装的typeorm来执行生成
3、 在生成的脚本中编写迁移sql语句(在up方法中写,down方法是用来revert的,暂时不了解使用场景);
// 例如我这个版本user表修改了字段名, name -> classname
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`alter table "user" rename column "name" TO "classname";`)
}
4、执行脚本;
npx typeorm migration:run // 如果没有生成ts文件,可以通过ts-node-dev执行,e.g. ts-node-dev ./node_modules/typeorm/cli.js migration:run
这个方案有一些优点,但也有他的缺点:
// 优点
本身对每次迁移命令是否执行都做了记录(记录在migrations这个表中),不需要开发者再自己进行判断版本执行对应的迁移命令;
// 缺点
整体流程过于繁琐和黑箱,不利于拓展和问题定位
迁移方案(自实现)
1、创建同步记录表;
// 表字段如下
id, // generate id
version, // 需要迁移的版本
executed, // 是否已执行
2、创建版本-迁移脚本映射表;
const migrationCliMap = [
{
version: '1.0.0',
cli: `ALTER TABLE "user" RENAME COLUMN "name" TO "classname";`
},
...
]
3、执行迁移并添加迁移记录;
// 获取最后一条记录
let _connectionManage = await jdbcMap.getCurrentDBAsync()
let _lastMigrationCli = await _connectionManage.getConnection(_connectionManage.db).getRepository('migration')
.query('SELECT * FROM migration WHERE executed=1 ORDER BY id DESC LIMIT 1;')
let _version = _lastMigrationCli.length ? _lastMigrationCli[0].version : '0.0.0'
// 遍历迁移脚本,执行未执行脚本
for(let i=0, len=migrationCliMap.length; i < len; i+=1) {
if(semver.gt(migrationCliMap[i].version, _version)) {
try{
await _connectionManage.getConnection(_connectionManage.db).getRepository('migration')
.query(migrationCliMap[i].cli)
await _connectionManage.getConnection(_connectionManage.db).getRepository('migration')
.createQueryBuilder()
.insert()
.values({
version: migrationCliMap[i].version,
executed: true
})
.execute()
} catch(err) {
logServices.errorLog(`数据库迁移失败,失败版本为: ${migrationCliMap[i].version}, 错误原因: ${err}`)
}
}
}
注:当数据库尚未第一次同步时,不要执行迁移,避免执行太多迁移命令甚至出错。