历史与版本
希望目标实体以及数据可以留存下来,并且恢复到指定的版本。
使用版本号
版本号是一种常见的版本设计方案,就是在要进行历史数据保留的表上面增加一个版本号字段,该字段可以是DateTime类型,也可以是int类型,每进行数据操作时,都是创建一个新的版本,版本是只增不减的,所以只需要拿到最大一个版本号,就能得到最新的业务数据。
版本号除了能够用于留存历史数据外,还有一个功能就是避免并发编辑操作。比如我们有一个对象A,当前的版本是1,两个用户同时打开了该对象的编辑页面,进行数据更改。先是甲用户提交更改,这个时候系统把对象的ID和版本进行查询,发现要修改的数据最新版本是1,所以成功修改,保存了对象A的新版本2。这个时候用户乙也提交了修改。系统把对象的ID和版本1进行查询,发现要修改的数据最新版本是2,不符合要求,所以拒绝用户乙的修改。用户乙只有刷新界面,拿到最新的版本2,再进行修改。
1
EXP123
100
1
2
EXP123
120
2
但是,使用这种方式也有很明显的缺点:
历史版本信息与当前版本信息放在一张表中,容易导致表数据过大。解决办法:
限制存储历史版本的数量,比如一个实体最多存储10条历史记录,如果超过10条,删除最老的版本,再新增一个新的版本;或者直接更新最老的版本为新版本
对表中的数据进行分表操作
查询列表、或者是分页信息的代码将会变得很难写,导致代码很难维护
增加标记字段(newtest),标记当前条目是最新的版本,在查询时增加这个条件逻辑
没法设置中间态,某些版本可能需要先入库,但是并不是立即生效的,比如实体处于审批中的状态,使用这种方式没法区分生效失效的情况
使用业务状态判断,比如业务实体的审批状态,如果处于审批中,获取时就将这个条目排除
单独增加增加一个状态,用以表示该条目是否生效
使用生效时间,失效时间
保存历史数据的第二办法是使用生效失效时间来表示一个版本。要进行历史数据记录的表增加“生效时间”“失效时间”两个字段,两个字段不允许为空。对于刚创建的数据,生效时间是创建该数据的时间,失效时间是9999-12-31。现在对这条数据进行了修改,那么我们只需要将当前时间设置为上一个版本的失效时间,同时创建一条新数据,生效时间是当前时间,失效时间是9999-12-31即可。
1
EXP123
100
2013/9/1 15:30:00
2013/9/9 15:00:00
2
EXP123
120
2013/9/9 15:00:00
9999/12/31 23:59:59
缺点:
历史版本信息与当前版本信息放在一张表中,容易导致表数据过大
查询时需要带着生效时间以及失效时间判断
无法直观知晓历史版本有多少个
使用版本历史表
使用历史表其实就是建立完全相同Schema的表(当然,也可以添加更多的字段用于记录额外的历史版本信息),该表只保留历史版本的数据。这有点像一个归档逻辑,所有历史版本我们认为都应该是不经常访问的,所有可以扔到单独的表,对于现有生效的版本,仍然保留在原表中,如果需要查询历史版本,那么就从历史表中查询。
使用单独的历史表有以下好处:
业务数据表的数据量不会因为历史版本记录而膨胀。因为历史数据都记录到了另外一个表中,所以业务数据表只记录了一份数据。
业务数据表的Schema不需要调整,增加额外的版本字段。由于对原有数据表不做Schema变更,所以原有查询逻辑也不用更改。对于一个现有的数据库设计,在增加历史数据记录功能时更简单。
业务数据表可以直接进行update操作,不会生成新的ID。由于ID不会变,所以我们并需要业务主键应用到程序逻辑中。
使用历史表记录历史版本主要是要对数据操作方法(增加、删除、修改)进行修改,使得每次数据操作时,先在历史表中留痕,然后再进行数据操作。另外就是对查询历史版本功能进行修改,因为历史数据在另外一个表中,所以对于的SQL是不一样的。当然,我们也可以创建历史版本数据库,里面保存了所有的历史表。
使用文档记录版本历史(不支持版本并发)
使用文档记录每个单据的版本历史,文档的内容格式可以是任意一种可序列化的数据结构。比如常用的Java、XML、YAML等。
不支持版本并发:某个单据不可能存在两个版本同时发生变更,故初始状态暂存只会有一个,只有当当前版本的生命周期结束后,才能新增加一个版本。
版本信息存储:t_change
t_changet_change 中记录的是某个单据的某个版本,不同的版本,他的版本号version以及版本编号changeCode不同,根据businessId以及businessType两个字段可以找到该笔业务单据的所有版本历史。
其中单条版本的内容如下:
版本的状态
一个版本也有他的生命周期,版本在创建后,如果发生了不同的操作,那么版本的状态转换也会发生不同,在遇到的实际项目设计中,版本的状态机图如下:

注意:
所有动作的执行都会校验当前change的状态,只有指定状态下才可做指定的操作
stash 暂存数据时,如果暂存数据是新的一条,那么会自动生成编号(唯一),并且生成version
版本的额外信息存储 Metadata
要想支持复杂的版本记录场景,单据记录版本历史是无法满足所有的需求的,故增加一个Metadata字段,他的类型与content字段类似,可以是任意结构。但是与content内容不同的是,metadata用来记录与版本本身相关的数据,比如:
版本的生成来源是手动还是自动
版本是否需要隐藏
版本的变更等级
版本的提交时间
版本的提交人
等等等。
示例:业务单据实现版本变更
业务单据会签状态

手动保存
自动新增
删除(略,只有为会签数据可被删除)
提交
审批撤回
审批拒绝
审批通过
业务的每个操作都会进行版本记录。
业务单据变更状态

变更保存
变更提交
变更审批撤销
变更审批拒绝
变更审批通过
变更与审批的关系
业务进行会签、变更操作时,有可能发生审批。如果该版本经过了审批,那么就会在metadata中设置一个标签:
如果workflowCode不为空,那么代表该版本经过了审批,有审批履历。
审批的发起是使用t_change的id + workflowCode 作为唯一标识;审批的workflowCode在审批一发起的时候就进行设置,后续无论进行任何操作,都不做更新。
变更等级
在业务发起变更时,根据变更内容的不同,需要决定不同的审批流;变更等级分为四种:
变更等级的计算是当前版本与上个published的版本相对比,以找出对应的等级。找出等级后,为了避免实时对比对性能有所损耗,会将变更等级存储在标签中:
业务自动新增和手动新增
决策在审批通过后,会生成关联的业务信息,自动生成的业务,即使是未会签也需要生成版本:
设置业务阶段为生成阶段,是为了区分该操作对应的业务操作阶段,以支持变更历史以及审批履历的标签信息,目前将阶段分为三个阶段:
设置生成来源,是为了标记业务的创建来源是什么,有手动创建与自动生成两种:
手动创建的业务会在暂存的时候设置生成来源为手动创建。
版本来源
一个版本的升级,可能是他本身发生变化导致的,也可能是由其他版本影响导致的。如果是受其他版本影响所导致的,需要设置来源的changeId。
比如,当决策发生变更时,其关联的业务会发生变更,这时候就需要记录这个版本的变更来源版本的changeId,已确保清楚每个变更的来源。
在决策影响点到业务处,会设置决策当前的changeId到业务影响版本。
作废来源
作废来源,代表这个版本被作废的原因,设置改元数据字段的主要意图有:
无论是审批撤回还是审批拒绝,change的状态都会变为
void已作废状态。为了区分作废倒是是因为什么操作作废的。撤销的版本在版本历史中隐藏,故需要此字段进行标记。
撤销与状态回退
需求要求在撤销后,重新生成暂存记录,并且业务的变更状态、审批状态,要回到上个状态中;比如,上个变更状态为变更拒绝,审批状态为已拒绝,用户发起变更后,再撤销,状态需要回到这两个状态中。
实现方式为,在版本生命周期结束时,记录业务的变更状态到metadata中:
撤销时,会查询上个版本,拿到状态与审批状态,并回退:
版本的隐藏
有些版本需要进行隐藏,比如业务变更,决策需要进行版本升级,就需要对版本进行隐藏处理。故增加metadata隐藏标记:
隐藏标记不会记录在版本历史中,具体参考ChangeByStageHistoryModel。
版本历史核心类ChangeByStageHistoryModel
ChangeByStageHistoryModelChangeByStageHistoryModel是一个以阶段为主要标记的版本历史帮助容器类:

历史记录是一个链表,链表以
version字段正序排序一个节点中可能包含多个版本,版本记录是一个
TreeSet,也已字段version正序排序节点的状态是由节点内的版本决定的:
changeId、changeCode、version、changeStatus,取节点内最大版本的信息
workflowCode则是从所有节点内的版本中取有workflowCode的第一条
position用于确定版本节点在历史中的位置,比如如果Node是位于第一位置的,列表则不显示版本编号
变更次数:统计变更阶段的Node数量
拥有隐藏标记的版本,不会记录到版本历史中。但是会存在于
originModels中,因为隐藏版本有可能需要使用,比如查询上个生效版本应该包含隐藏版本。
版本依赖
场景:业务单据存在包含关系,且单据之间会互相影响。
假设决策模型下包含多个合同模型对象:
解决方案1,在父单据中冗余子的业务单据,缺点明显:
需要做数据刷新同步
冗余数据造成额外磁盘以及内存的空间消耗
父单据版本与子单据版本的对应关系,只能通过子单据版本的
fromChangeId决定,如果想找到当前版本父业务单据找到关联的子业务单据的对应版本,是无法做到的
解决方案2,父单据只存储自己的信息,通过版本id关联业务版本:
单据版本信息解耦,不需要做数据刷新同步
不会产生冗余数据
可以通过版本索引找到每个子业务单据的版本以及版本的全部内容
代码开发复杂度变高,保存父业务单据需要提前存储其关联的所有业务单据后,并将他们的id刷入到父业务单据中,才可以完成版本刷新操作
版本在序列化、返序列化时,需要做拆分、组装等操作,对性能有一定的影响
方式一:依赖索引信息存储到content,并使用一个特殊的对象表示
这种方式可以做到代码上的无感知:
定义业务模型时,定义业务的所有字段
查询业务模型是,会自动将所有的版本引用替换为完整的业务信息
也拥有一定的缺点:
版本关联关系被耦合到业务模型中(问题不大)
如果子单据发生变更,需要更新父单据的变更content,更新时需要拿到父单据的全量信息(繁琐,mysql是这样,mongo应该可以少拿一些)
假设决策模型下包含多个业务模型对象:
在执行序列化时,需要将ContractModel的内容替换为引用id,也就是说,序列化后的内容如下:
而将上述json反序列化后,需要还原成完整的数据对象信息:
冗余业务信息,以实现目标数据定位
子业务更新后,可能需要刷新父业务中自己的版本id,比如需要刷新 cotnractId 为100的数据的版本从2变为 4,由于父业务中指存储了引用id,他无法定位具体的更新位置的,故冗余change的其他关键信息:businessId和businessType,用于定位具体的更新位置:
引用标识接口 ChangeReference
定义引用标识接口,所有需要做引用处理的业务模型类,都需要实现该接口:
还提供了一个基本实现类,用来表示一个没有任何业务信息的简单引用,该类适合只需要记录版本关系的情况:
版本嵌套的层级限制
版本和版本之间可以相互关联是由前提得:
版本与版本的依赖不能出现循环依赖
版本嵌套的层级不能超过三层,比如决策依赖业务 这是二层, 决策依赖业务、业务依赖土地信息,这是三层
使用序列化、返序列化以支持版本嵌套
在ChangeManager中提供两个方法,分别用于序列化、反序列化:
如果有任何类实现了ChangeReference接口,并且他引用是一个有效的引用信息,就认为这是一个有效的版本索引;那么在序列化时,只会存储版本信息,其他业务信息不会做存储;同样的,在反序列化时,如果目标类型是一个ChangeReference类型,并且返序列化后的对象是一个有效的版本引用,那么就会通过反射的方式收集所有的引用id,批量查询content,然后将content反序列化为对应的类型。
嵌套版本的单据的各种操作
假设父业务单据为决策,子业务单据为业务,决策操作时会携带所有的业务信息,模拟他们的操作时的版本行为如下:
决策保存,决策含有完整的业务信息
决策操作时不会携带所有的业务信息,他们是分开操作的,版本行为如下:
决策保存,决策只有业务的版本信息,不包含完整的业务信息
序列化、返序列化代码实现样例(Jackson 与 反射)
序列化时,如果当前对象是一个有效的版本引用,那么序列化时,忽略该对象的其他所有字段,只序列化版本引用信息入库:
反序列化时,在Jackson处理后,使用反射 + 递归的方式获取所有的 changeReferenceId,然后从库中查询到所有的版本信息,并填充到目标对象中:
方式二:依赖索引信息单独存储到一个字段中
在t_change表中增加字段 relationship,用以记录当前版本关联的所有版本:
references
json
他的类型是一个json类型,你可以设计该类型为任意一种结构,比如设计为一个id数组:
也可以这样:
这种方式的缺点为:
业务端需要自己将索引信息填充到业务信息中
在替换子业务(业务)的版本,需要在父业务(决策)版本维度操作,将所有的id生成好后再重新刷入
如果是数组方式,业务端还要判断每个索引的id是属于什么类型的业务的,并对他们进行分组填入
从父单据可以很容易的通过
relationship找到关联子单据的对应的版本,但是从子单据,很难获取父单据的版本。所以数组方式所表现的数据场景有限,可以使用自定义的json结构来存储表示,比如:具体处理方式由业务端决定。
最后更新于
这有帮助吗?