数据变更的场景

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

数据变更的策略

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

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

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

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

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

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

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

定义数据版本

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

  1. 1{
  2. 2 "name": "egg-born-module-test-party",
  3. 3 "version": "4.0.8",
  4. 4 "eggBornModule": {
  5. 5 "fileVersion": 1
  6. 6 }
  7. 7}

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

Bean组件:version.manager

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

- Bean组件定义

src/suite-vendor/test-party/modules/test-party/backend/src/bean/version.manager.js

  1. 1const VersionTestFn = require('./version/test.js');
  2. 2
  3. 3module.exports = app => {
  4. 4
  5. 5 class Version extends app.meta.BeanBase {
  6. 6
  7. 7 async update(options) {
  8. 8 // update
  9. 9 if (options.version === 1) {
  10. 10 let sql = `
  11. 11 CREATE TABLE testParty (
  12. 12 id int(11) NOT NULL AUTO_INCREMENT,
  13. 13 createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  14. 14 updatedAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  15. 15 deleted int(11) DEFAULT '0',
  16. 16 iid int(11) DEFAULT '0',
  17. 17 atomId int(11) DEFAULT '0',
  18. 18 personCount int(11) DEFAULT '0',
  19. 19 partyTypeId int(11) DEFAULT '0',
  20. 20 PRIMARY KEY (id)
  21. 21 )
  22. 22 `;
  23. 23 await this.ctx.model.query(sql);
  24. 24
  25. 25 sql = `
  26. 26 CREATE TABLE testPartyType (
  27. 27 id int(11) NOT NULL AUTO_INCREMENT,
  28. 28 createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  29. 29 updatedAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  30. 30 deleted int(11) DEFAULT '0',
  31. 31 iid int(11) DEFAULT '0',
  32. 32 name varchar(255) DEFAULT NULL,
  33. 33 PRIMARY KEY (id)
  34. 34 )
  35. 35 `;
  36. 36 await this.ctx.model.query(sql);
  37. 37
  38. 38 sql = `
  39. 39 CREATE VIEW testPartyView as
  40. 40 select a.*,b.name as partyTypeName from testParty a
  41. 41 left join testPartyType b on a.partyTypeId=b.id
  42. 42 `;
  43. 43 await this.ctx.model.query(sql);
  44. 44
  45. 45 }
  46. 46 }
  47. 47
  48. 48 async init(options) {
  49. 49 // init
  50. 50 if (options.version === 1) {
  51. 51 // types
  52. 52 for (const name of [ 'Birthday', 'Dance', 'Garden' ]) {
  53. 53 await this.ctx.model.partyType.insert({ name });
  54. 54 }
  55. 55 // add role rights
  56. 56 const roleRights = [
  57. 57 { roleName: 'system', action: 'create' },
  58. 58 { roleName: 'system', action: 'read', scopeNames: 'authenticated' },
  59. 59 { roleName: 'system', action: 'write', scopeNames: 0 },
  60. 60 { roleName: 'system', action: 'delete', scopeNames: 0 },
  61. 61 { roleName: 'system', action: 'clone', scopeNames: 0 },
  62. 62 { roleName: 'system', action: 'deleteBulk' },
  63. 63 { roleName: 'system', action: 'exportBulk' },
  64. 64 ];
  65. 65 await this.ctx.bean.role.addRoleRightBatch({ atomClassName: 'party', roleRights });
  66. 66 }
  67. 67
  68. 68 }
  69. 69
  70. 70 async test() {
  71. 71 const versionTest = new (VersionTestFn(this.ctx))();
  72. 72 await versionTest.run();
  73. 73 }
  74. 74
  75. 75 }
  76. 76
  77. 77 return Version;
  78. 78};
名称 说明
options.version 只需针对模块的不同数据版本编写相应的变更逻辑,系统会根据当前数据版本自动调用需要升级变更的部分
名称 说明
update 实例无关的数据架构变更
init 实例相关的数据变更,比如初始化一些内置角色的授权
test 仅在测试环境执行,向数据库灌入测试用的种子数据,比如为后续的单元测试提供初始测试数据初始角色授权
  • updateinit的区别
  1. CabloyJS启动一个服务,可以支持多个实例运行。实例共享数据表结构,但运行中产生的数据是相互隔离的
  2. update处理与实例无关的数据架构变更,如创建业务数据表testParty,以及视图、存储过程、函数、索引等一系列数据架构
  3. init处理与实例相关的数据变更,如添加角色的原子授权

- Bean组件注册

src/suite-vendor/test-party/modules/test-party/backend/src/beans.js

  1. 1const versionManager = require('./bean/version.manager.js');
  2. 2
  3. 3module.exports = app => {
  4. 4 const beans = {
  5. 5 // version
  6. 6 'version.manager': {
  7. 7 mode: 'app',
  8. 8 bean: versionManager,
  9. 9 },
  10. 10 };
  11. 11 return beans;
  12. 12};

最佳实践

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

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

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

  1. 1# 重建数据库 + 单元测试
  2. 2$ npm run test:backend
  3. 3# 或者
  4. 4# 仅重建数据库
  5. 5$ npm run db:reset

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

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

延伸阅读

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