sqlite迁移方案(migration)
背景
先说下背景,在使用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 _lastMigrationCli = await _connectionManage.getConnection(_connectionManage.db).getRepository('migration') .query('select * from migration where executed=false order by id desc limit 1;') let _lastMigrationCliVersion = _lastMigrationCli.version || '0.0.0' // 遍历所有记录,从低到高将未执行的迁移命令执行后,添加到同步记录表中 for(let i=0, len=migrationCliMap.length; i < len; i+=1) { if(semver.gt(migrationCliMap[i].version, _lastMigrationCliVersion)) { 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() } }复制代码
注:当数据库尚未第一次同步时,不要执行迁移,避免执行太多迁移命令甚至出错。
作者:vb
链接:https://juejin.cn/post/7054452425519792136