数据变更的场景

模块一般都有与业务相关的数据架构,比如模块test-party有一个业务数据表testParty,其中包含若干字段。当模块编译并部署发布后,如果模块数据架构需要调整(比如给某个数据表添加一个字段),应该怎么设计这种数据变更的机制呢?

数据变更的策略

策略1: 使用SQL文件进行数据迁移

有的框架根据数据的变更生成一系列SQL文件,通过导入SQL文件实现数据的迁移。这种策略主要存在两个问题:

  1. 面对复杂变更力不从心:由于在实际的业务场景中,数据的变更逻辑非常复杂,比如删除一个字段,可能还需要联动对其他的业务数据做一些调整。如果仅仅使用SQL语句来表达这些业务数据变更,往往显得力不从心

  2. 无法便利的支持多实例多租户场景:比如有1000个租户实例,每个实例都需要初始化自己的数据。基于性能考虑,不可能在系统启动时同时执行这1000实例的初始化逻辑,而是要按需执行。具体而言,就是系统启动时不执行实例的初始化逻辑,什么时候有前端用户访问某个实例的接口服务时,才会执行该实例的初始化逻辑

策略2: 使用JS代码进行数据变更

CabloyJS采用JS代码来管理数据变更的逻辑,具体而言就是通过一个Bean组件集中管理模块数据变更的逻辑。当模块编译并部署发布后,模块当前的数据版本处于封闭状态。如果有新的数据架构变更,只需要递增模块的数据版本,然后在Bean组件中实现变更逻辑

这样,当系统启动时,就会自动检测模块数据版本是否有变化;如果有变化,就会执行Bean组件的升级逻辑,从而完成数据架构的无缝升级

定义数据版本

在模块的package.json文件中配置fileVersion为当前数据版本

{
  "name": "egg-born-module-test-party",
  "version": "4.0.8",
  "eggBornModule": {
    "fileVersion": 1
  }
}

当模块已经发布后,下次再发生数据架构变更时,fileVersion需要递增+1

Bean组件:version.manager

与模块相关的数据架构变更管理,都在Bean组件version.manager

- Bean组件定义

src/module/test-party/backend/src/bean/version.manager.js

const VersionTestFn = require('./version/test.js');

module.exports = app => {

  class Version extends app.meta.BeanBase {

    async update(options) {
      // update
      if (options.version === 1) {
        let sql = `
          CREATE TABLE testParty (
            id int(11) NOT NULL AUTO_INCREMENT,
            createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updatedAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            deleted int(11) DEFAULT '0',
            iid int(11) DEFAULT '0',
            atomId int(11) DEFAULT '0',
            personCount int(11) DEFAULT '0',
            partyTypeId int(11) DEFAULT '0',
            PRIMARY KEY (id)
          )
        `;
        await this.ctx.model.query(sql);

        sql = `
          CREATE TABLE testPartyType (
            id int(11) NOT NULL AUTO_INCREMENT,
            createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updatedAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            deleted int(11) DEFAULT '0',
            iid int(11) DEFAULT '0',
            name varchar(255) DEFAULT NULL,
            PRIMARY KEY (id)
          )
        `;
        await this.ctx.model.query(sql);

        sql = `
          CREATE VIEW testPartyView as
            select a.*,b.name as partyTypeName from testParty a
              left join testPartyType b on a.partyTypeId=b.id
        `;
        await this.ctx.model.query(sql);

      }
    }

    async init(options) {
      // init
      if (options.version === 1) {
        // types
        for (const name of [ 'Birthday', 'Dance', 'Garden' ]) {
          await this.ctx.model.partyType.insert({ name });
        }
        // add role rights
        const roleRights = [
          { roleName: 'system', action: 'create' },
          { roleName: 'system', action: 'read', scopeNames: 'authenticated' },
          { roleName: 'system', action: 'write', scopeNames: 0 },
          { roleName: 'system', action: 'delete', scopeNames: 0 },
          { roleName: 'system', action: 'clone', scopeNames: 0 },
          { roleName: 'system', action: 'deleteBulk' },
          { roleName: 'system', action: 'exportBulk' },
        ];
        await this.ctx.bean.role.addRoleRightBatch({ atomClassName: 'party', roleRights });
      }

    }

    async test() {
      const versionTest = new (VersionTestFn(this.ctx))();
      await versionTest.run();
    }

  }

  return Version;
};
名称 说明
options.version 只需针对模块的不同数据版本编写相应的变更逻辑,系统会根据当前数据版本自动调用需要升级变更的部分
名称 说明
update 实例无关的数据架构变更
init 实例相关的数据变更,比如初始化一些内置角色的授权
test 仅在测试环境执行,进行与测试相关的数据初始化工作,比如为后续的单元测试提供初始测试数据初始角色授权
  • updateinit的区别
  1. CabloyJS启动一个服务,可以支持多个实例运行。实例共享数据表结构,但运行中产生的数据是相互隔离的
  2. update处理与实例无关的数据架构变更,如创建业务数据表testParty,以及视图、存储过程、函数、索引等一系列数据架构
  3. init处理与实例相关的数据变更,如添加角色的原子授权

- Bean组件注册

src/module/test-party/backend/src/beans.js

const versionManager = require('./bean/version.manager.js');

module.exports = app => {
  const beans = {
    // version
    'version.manager': {
      mode: 'app',
      bean: versionManager,
    },
  };
  return beans;
};

最佳实践

当已经发布的模块需要再次变更数据架构时,我们需要将模块package.json中的eggBornModule.fileVersion递增+1

由于是在开发过程当中,免不了需要不断的修改bean组件version.manager中的升级逻辑。那么,如何让这些不断修改的数据变更在数据库中生效呢?

有人说打开数据库管理工具进行手工修改。而CabloyJS提供了一种更加便利的方法,只需执行一遍单元测试,就会自动化重建数据库,方法如下:

$ npm run test:backend

这也是bean组件version.manager提供test方法的意义所在:当执行单元测试的时候,会自动执行test方法初始化一些测试数据,方便我们测试和开发

比如,模块test-party就提供了一些测试角色、测试用户、测试权限。当单元测试完成后,数据库里就有了这些基础数据,我们就可以直接进入业务的测试环节,而不是通过手工来重新输入这些基础数据

延伸阅读

运行单元测试就会自动重建测试数据库,这涉及到CabloyJS一个核心概念:数据库规划,请与本文参照着阅读: