sqlite迁移方案(migration)

1,413 阅读4分钟

背景

先说下背景,在使用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}`)
         }
        }
    }

注:当数据库尚未第一次同步时,不要执行迁移,避免执行太多迁移命令甚至出错。