希望目标实体以及数据可以留存下来,并且恢复到指定的版本。
使用版本号
版本号是一种常见的版本设计方案,就是在要进行历史数据保留的表上面增加一个版本号字段,该字段可以是DateTime类型,也可以是int类型,每进行数据操作时,都是创建一个新的版本,版本是只增不减的,所以只需要拿到最大一个版本号,就能得到最新的业务数据。
版本号除了能够用于留存历史数据外,还有一个功能就是避免并发编辑操作。比如我们有一个对象A,当前的版本是1,两个用户同时打开了该对象的编辑页面,进行数据更改。先是甲用户提交更改,这个时候系统把对象的ID和版本进行查询,发现要修改的数据最新版本是1,所以成功修改,保存了对象A的新版本2。这个时候用户乙也提交了修改。系统把对象的ID和版本1进行查询,发现要修改的数据最新版本是2,不符合要求,所以拒绝用户乙的修改。用户乙只有刷新界面,拿到最新的版本2,再进行修改。
但是,使用这种方式也有很明显的缺点:
历史版本信息与当前版本信息放在一张表中,容易导致表数据过大。解决办法:
限制存储历史版本的数量,比如一个实体最多存储10条历史记录,如果超过10条,删除最老的版本,再新增一个新的版本;或者直接更新最老的版本为新版本
查询列表、或者是分页信息的代码将会变得很难写,导致代码很难维护
增加标记字段(newtest),标记当前条目是最新的版本,在查询时增加这个条件逻辑
没法设置中间态,某些版本可能需要先入库,但是并不是立即生效的,比如实体处于审批中的状态,使用这种方式没法区分生效失效的情况
使用业务状态判断,比如业务实体的审批状态,如果处于审批中,获取时就将这个条目排除
使用生效时间,失效时间
保存历史数据的第二办法是使用生效失效时间来表示一个版本。要进行历史数据记录的表增加“生效时间”“失效时间”两个字段,两个字段不允许为空。对于刚创建的数据,生效时间是创建该数据的时间,失效时间是9999-12-31。现在对这条数据进行了修改,那么我们只需要将当前时间设置为上一个版本的失效时间,同时创建一条新数据,生效时间是当前时间,失效时间是9999-12-31即可。
缺点:
历史版本信息与当前版本信息放在一张表中,容易导致表数据过大
使用版本历史表
使用历史表其实就是建立完全相同Schema的表(当然,也可以添加更多的字段用于记录额外的历史版本信息),该表只保留历史版本的数据。这有点像一个归档逻辑,所有历史版本我们认为都应该是不经常访问的,所有可以扔到单独的表,对于现有生效的版本,仍然保留在原表中,如果需要查询历史版本,那么就从历史表中查询。
使用单独的历史表有以下好处:
业务数据表的数据量不会因为历史版本记录而膨胀。因为历史数据都记录到了另外一个表中,所以业务数据表只记录了一份数据。
业务数据表的Schema不需要调整,增加额外的版本字段。由于对原有数据表不做Schema变更,所以原有查询逻辑也不用更改。对于一个现有的数据库设计,在增加历史数据记录功能时更简单。
业务数据表可以直接进行update操作,不会生成新的ID。由于ID不会变,所以我们并需要业务主键应用到程序逻辑中。
使用历史表记录历史版本主要是要对数据操作方法(增加、删除、修改)进行修改,使得每次数据操作时,先在历史表中留痕,然后再进行数据操作。另外就是对查询历史版本功能进行修改,因为历史数据在另外一个表中,所以对于的SQL是不一样的。当然,我们也可以创建历史版本数据库,里面保存了所有的历史表。
使用文档记录版本历史(不支持版本并发)
使用文档记录每个单据的版本历史,文档的内容格式可以是任意一种可序列化的数据结构。比如常用的Java、XML、YAML等。
不支持版本并发:某个单据不可能存在两个版本同时发生变更,故初始状态暂存只会有一个,只有当当前版本的生命周期结束后,才能新增加一个版本。
版本信息存储:t_change
t_change
中记录的是某个单据的某个版本,不同的版本,他的版本号version
以及版本编号changeCode
不同,根据businessId
以及businessType
两个字段可以找到该笔业务单据的所有版本历史。
其中单条版本的内容如下:
public class ChangeModel {
/**
* 版本记录主键
*/
private Long id;
/**
* 版本记录唯一编号
*/
private String changeCode;
/**
* 版本号(int,自增,从1开始)
*/
private Integer version;
/**
* 版本所属单据的id
*/
private Long businessId;
/**
* 版本所属单据的类型
*/
private String businessType;
/**
* 版本内容(全量),里面是序列化后的单据json信息
*/
private String content;
/**
* 版本的其他元信息(json数据)
*/
private Metadata metadata;
/**
* 版本目前的状态
*/
private String changeStatus;
}
版本的状态
一个版本也有他的生命周期,版本在创建后,如果发生了不同的操作,那么版本的状态转换也会发生不同,在遇到的实际项目设计中,版本的状态机图如下:
注意:
所有动作的执行都会校验当前change的状态,只有指定状态下才可做指定的操作
stash 暂存数据时,如果暂存数据是新的一条,那么会自动生成编号(唯一),并且生成version
要想支持复杂的版本记录场景,单据记录版本历史是无法满足所有的需求的,故增加一个Metadata字段,他的类型与content
字段类似,可以是任意结构。但是与content
内容不同的是,metadata
用来记录与版本本身相关的数据,比如:
等等等。
示例:业务单据实现版本变更
业务单据会签状态
手动保存
// 暂存用户提交内容,如果是第一次就会插入,否则更新暂存的版本
changeManager.stash(change);
自动新增
// 自动新增的数据可能来自其他单据,故需要做版本记录
changeManager.stash(change);
changeManager.commit(change);
changeManager.publish(change);
提交
changeManager.commit(commit);
审批撤回
changeManager.voids(change);
changeManager.stash(newChange);
审批拒绝
changeManager.voids(change);
审批通过
changeManager.publish(change);
业务的每个操作都会进行版本记录。
业务单据变更状态
变更保存
changeManager.stash(change);
变更提交
changeManager.commit(change);
变更审批撤销
changeManager.voids(change);
changeManager.stash(newChange);
变更审批拒绝
changeManager.voids(change);
变更审批通过
changeManager.publish(change);
变更与审批的关系
业务进行会签、变更操作时,有可能发生审批。如果该版本经过了审批,那么就会在metadata中设置一个标签:
// 审批流代码
String workflowCode;
如果workflowCode不为空,那么代表该版本经过了审批,有审批履历。
审批的发起是使用t_change
的id
+ workflowCode
作为唯一标识;审批的workflowCode
在审批一发起的时候就进行设置,后续无论进行任何操作,都不做更新。
change.getMetadata().setWorkflowCode("xxxxWorkflow");
changeManager.commit(change);
startProcess(changeId, workflowCode, xxxx);
变更等级
在业务发起变更时,根据变更内容的不同,需要决定不同的审批流;变更等级分为四种:
0级变更 无变更,提示没有做出修改,不允许变更
1级变更 走完整的审批流程
2级变更 走简略的审批流程
3级变更 不走审批流程
变更等级的计算是当前版本与上个published
的版本相对比,以找出对应的等级。找出等级后,为了避免实时对比对性能有所损耗,会将变更等级存储在标签中:
// 变更等级在版本一开始暂存的时候就会计算好,并填入到metadata中
// 每次暂存都会重新计算一次
change.getMetadata().setChangeLevel(level);
changeManager.stash(change);
业务自动新增和手动新增
决策在审批通过后,会生成关联的业务信息,自动生成的业务,即使是未会签也需要生成版本:
// 自动新增一个业务
contractManager.submitAuto(contract);
// 设置标签信息
changeModel.getMetadata()
.setBusinessOperateStage(BusinessOperateStageEnum.GENERATE)
.setGenerateSource(BusinessGenerateSource.AUTO);
设置业务阶段为生成阶段,是为了区分该操作对应的业务操作阶段,以支持变更历史以及审批履历的标签信息,目前将阶段分为三个阶段:
GENERATE("generate", "生成阶段"),
CHANGE("change", "变更阶段"),
TERMINAL("terminal", "终止阶段"),
设置生成来源,是为了标记业务的创建来源是什么,有手动创建与自动生成两种:
// 由其他业务自动生成的,自动生成
AUTO("auto", "自动生成"),
MANUAL("manual", "手动创建"),
手动创建的业务会在暂存的时候设置生成来源为手动创建。
版本来源
private Long fromOtherChangeId;
一个版本的升级,可能是他本身发生变化导致的,也可能是由其他版本影响导致的。如果是受其他版本影响所导致的,需要设置来源的changeId
。
比如,当决策发生变更时,其关联的业务会发生变更,这时候就需要记录这个版本的变更来源版本的changeId,已确保清楚每个变更的来源。
在决策影响点到业务处,会设置决策当前的changeId
到业务影响版本。
作废来源
private BusinessVoidSourceEnum voidSource;
UNDO("undo", "撤销"),
REFUSED("refused", "拒绝"),
作废来源,代表这个版本被作废的原因,设置改元数据字段的主要意图有:
无论是审批撤回还是审批拒绝,change的状态都会变为void
已作废状态。为了区分作废倒是是因为什么操作作废的。
撤销的版本在版本历史中隐藏,故需要此字段进行标记。
撤销与状态回退
需求要求在撤销后,重新生成暂存记录,并且业务的变更状态、审批状态,要回到上个状态中;比如,上个变更状态为变更拒绝
,审批状态为已拒绝
,用户发起变更后,再撤销,状态需要回到这两个状态中。
实现方式为,在版本生命周期结束时,记录业务的变更状态到metadata中:
/**
* 业务变更状态(仅业务使用)
*/
private ContractChangeStatusEnum contractChangeStatus;
// 审批通过、审批拒绝、审批撤回时,会设置此状态
changeModel.getMetadata()
.setVoidSource(BusinessVoidSourceEnum.REFUSED)
.setMotionChangeStatus(BaseEnum.getEnum(motionModel.getChangeStatus(), MotionChangeStatusEnum.class));
changeManager.voids(changeModel);
撤销时,会查询上个版本,拿到状态与审批状态,并回退:
Integer lastApprovalStatus;
// 获取上个审批状态
LinkedList<ChangeByStageHistoryModel.Node> history = changeManager.findChangeHistory(contractModel.getId(), CHANGE_BUSINESS_CONTRACT).historyHasWorkflowCode();
ChangeByStageHistoryModel.Node lastHasWorkflowNode = CollUtil.get(history, -2);
// 如果上次拥有审批的变更记录不存在,说明该笔业务始终没有产生审批流,设置审批状态为空
if (Objects.isNull(lastHasWorkflowNode)) {
lastApprovalStatus = null;
} else {
List<ChangeModel> hasWorkflowNodes = lastHasWorkflowNode.getRecords()
.stream()
.filter(r -> Objects.nonNull(r.getMetadata().getWorkflowCode()))
.collect(Collectors.toList());
if (hasWorkflowNodes.isEmpty()) {
lastApprovalStatus = null;
} else {
ChangeModel hasWorkflowRecord = CollUtil.get(hasWorkflowNodes, -1);
ProcessModel lastProcess = processManager.findOne(hasWorkflowRecord.getId(), hasWorkflowRecord.getMetadata().getWorkflowCode());
AssertUtil.notNull(lastProcess, "执行变更撤销失败,未查询到上个版本的审批信息, changeId=" + newestVersion.getId());
lastApprovalStatus = lastProcess.getApprovalStatus();
}
}
//撤销变更
contractManager.changeRevoke(contractModel, lastApprovalStatus);
newestVersion.getMetadata()
.setVoidSource(BusinessVoidSourceEnum.UNDO);
changeManager.voids(newestVersion);
版本的隐藏
有些版本需要进行隐藏,比如业务变更,决策需要进行版本升级,就需要对版本进行隐藏处理。故增加metadata隐藏标记:
/**
* 隐藏标记,如果该字段不为空,代表该条变更记录需要隐藏
*/
private YesOrNoEnum hiddenFlag = YesOrNoEnum.NO;
隐藏标记不会记录在版本历史中,具体参考ChangeByStageHistoryModel
。
版本历史核心类ChangeByStageHistoryModel
ChangeByStageHistoryModel
是一个以阶段为主要标记的版本历史帮助容器类:
历史记录是一个链表,链表以version
字段正序排序
一个节点中可能包含多个版本,版本记录是一个TreeSet
,也已字段version
正序排序
节点的状态是由节点内的版本决定的:
changeId、changeCode、version、changeStatus,取节点内最大版本的信息
workflowCode则是从所有节点内的版本中取有workflowCode的第一条
position用于确定版本节点在历史中的位置,比如如果Node是位于第一位置的,列表则不显示版本编号
拥有隐藏标记的版本,不会记录到版本历史中。但是会存在于 originModels
中,因为隐藏版本有可能需要使用,比如查询上个生效版本应该包含隐藏版本。
/**
* 变更历史业务对象
*
* @author yangshunxiang
* @since 2023/9/21
*/
@Slf4j
public class ChangeByStageHistoryModel {
private final List<ChangeModel> originModels;
public ChangeByStageHistoryModel(List<ChangeModel> originModels) {
this.originModels = Optional.ofNullable(originModels).orElse(Collections.emptyList());
}
private LinkedList<Node> history = null;
private BusinessOperateStageEnum currentStage = BusinessOperateStageEnum.GENERATE;
/**
* 根据changeId 找到对应的节点
*
* @param changeId 变更id
* @return 节点
*/
public Node findNodeById(Long changeId) {
return history().stream()
.filter(node -> Objects.equals(changeId, node.getChangeId()))
.findFirst()
.orElse(null);
}
/**
* 变更历史中的一个节点,目前在生成阶段中,一个节点可能对应多个change记录,但是他们一定是同一阶段
*/
@SuppressWarnings("UnusedReturnValue")
public static class Node {
/**
* 版本在版本历史中的位置,从0开始
*/
@Getter
private final Integer position;
/**
* 节点所处的阶段
*/
@Getter
private final BusinessOperateStageEnum stage;
/**
* 变更记录列表
*/
@Getter
private final TreeSet<ChangeModel> records = new TreeSet<>(Comparator.comparingInt(ChangeModel::getVersion));
/**
* 节点在创建时,至少拥有一个变更记录
*
* @param position 位置
*/
public Node(Integer position, ChangeModel changeModel) {
AssertUtil.notNull(position, "节点创建时,节点在历史中的位置不可为空");
AssertUtil.notNull(changeModel, "节点创建时,初始记录不可为空");
AssertUtil.notNull(changeModel.getMetadata().getBusinessOperateStage(), "节点创建时,初始记录的阶段不可为空");
this.position = position;
this.records.add(changeModel);
this.stage = changeModel.getMetadata().getBusinessOperateStage();
}
/**
* 往节点中插入一条变更记录
*
* @param changeModel 变更记录
* @return 节点对象以链式调用
*/
public Node addRecord(ChangeModel changeModel) {
if (stage != changeModel.getMetadata().getBusinessOperateStage()) {
log.error("变更历史节点在插入节点时,出现阶段不匹配的情况,当前节点的阶段为 {},目标插入记录的阶段为 {},目标change记录的id为 {}",
stage,
changeModel.getMetadata().getBusinessOperateStage(),
changeModel.getId()
);
throw new ServiceException("变更历史节点在插入节点时,出现阶段不匹配的情况");
}
records.add(changeModel);
return this;
}
/**
* 获取记录中的最后一条(版本号最大的一条)
*
* @return changeModel
*/
public ChangeModel lastRecord() {
return this.records.last();
}
public Long getChangeId() {
return lastRecord().getId();
}
/**
* 节点的变更编号,目前取节点钟最大版本的变更记录的编号
* (以下字段类似)
*
* @return changeCode
*/
public String getChangeCode() {
return lastRecord().getChangeCode();
}
public Integer getVersion() {
return lastRecord().getVersion();
}
public Long getBusinessId() {
return lastRecord().getBusinessId();
}
public String getChangeType() {
return lastRecord().getChangeType();
}
public String getChangeStatus() {
return lastRecord().getChangeStatus();
}
public ChangeModel.Metadata getMetadata() {
return lastRecord().getMetadata();
}
/**
* 获取阶段的workflow code, 一个阶段只会有一个workflow code
* @return 如果该阶段没有发生过任何的审批流,那么workflow code返回空
*/
public String getStageWorkflowCode() {
return this.records
.stream()
.filter(cm -> Objects.nonNull(cm.getMetadata().getWorkflowCode()))
.map(cm -> cm.getMetadata().getWorkflowCode())
.findFirst()
.orElse(null);
}
}
public LinkedList<Node> history() {
if (Objects.isNull(history)) {
history = new LinkedList<>();
originModels.stream()
.sorted(Comparator.comparingInt(ChangeModel::getVersion))
.forEach(this::addHistory);
}
return history;
}
public LinkedList<Node> historyWithoutStash() {
return history().stream()
.filter(c -> !Objects.equals(c.getChangeStatus(), ChangeMainStatusEnum.STASHED.getCode()))
.collect(Collectors.toCollection(LinkedList::new));
}
/**
* 仅仅获取拥有审批流的变更历史节点
* @return 节点历史
*/
public LinkedList<Node> historyHasWorkflowCode() {
return history().stream()
.filter(c -> Objects.nonNull(c.getStageWorkflowCode()))
.collect(Collectors.toCollection(LinkedList::new));
}
/**
* 变更条数
* 1. 生成阶段的数据不算变更
* 2. 草稿状态的变更也算作变更记录
*
* @return 条数
*/
public Integer changedHistoryCount() {
return history().stream()
.filter(c -> c.getMetadata().getBusinessOperateStage() != BusinessOperateStageEnum.GENERATE)
.collect(Collectors.toCollection(LinkedList::new))
.size();
}
/**
* 注意 一定要按照版本从小到大的顺序添加元素
*
* @param changeModel element whose presence in this collection is to be ensured
*/
private void addHistory(ChangeModel changeModel) {
if (changeModel == null) {
log.error("插入ChangeList失败,元素值为空");
return;
}
ChangeMainStatusEnum changeStatus = BaseEnum.getEnum(changeModel.getChangeStatus(), ChangeMainStatusEnum.class);
if (Objects.isNull(changeStatus)) {
log.error("插入ChangeList失败,变更状态为空,changeId={}", changeModel.getId());
return;
}
BusinessOperateStageEnum stage = changeModel.getMetadata().getBusinessOperateStage();
// 不允许变更阶段为空的数据插入
if (Objects.isNull(stage)) {
log.error("插入ChangeList失败,变更阶段为空,changeId={}", changeModel.getId());
return;
}
if (changeModel.getMetadata().getHiddenFlag() == YesOrNoEnum.YES) {
log.error("插入ChangeList失败,当前的change记录是隐藏的状态,不会被记录到历史中,changeId={}", changeModel.getId());
return;
}
if (changeModel.getMetadata().getBusinessOperateStage() != BusinessOperateStageEnum.GENERATE
&& changeStatus == ChangeMainStatusEnum.VOID
&& changeModel.getMetadata().getVoidSource() == BusinessVoidSourceEnum.UNDO) {
log.warn("插入ChangeList跳过,非生成阶段,变更状态为作废,且作废来源为撤销的不能加入变更历史中");
return;
}
// 如果新入的元素为生成阶段,那么当前阶段为生成阶段才能插入,否则不能插入
if (stage == BusinessOperateStageEnum.GENERATE && currentStage != BusinessOperateStageEnum.GENERATE) {
log.error("插入ChangeList失败,当前阶段为 {}, 不能拿插入生成阶段数据, changeId = {}", currentStage.getMessage(), changeModel.getId());
return;
}
// 插入生成阶段数据
if (stage == BusinessOperateStageEnum.GENERATE) {
// 如果是第一条,直接插入
if (history.size() == 0) {
history.add(new Node(0, changeModel));
return;
}
// 否则进行生成阶段的追加
history.getLast().addRecord(changeModel);
return;
}
// 插入变更、终止阶段数据
currentStage = stage;
history.add(new Node(history.size(), changeModel));
}
/**
* 获取最大版本
*
* @return 最大版本
*/
public Node maxVersion() {
return history().peekLast();
}
/**
* 最大版本不包含暂存数据
*
* @return 最大版本
*/
public Node maxVersionWithoutStash() {
return history().stream()
.filter(node -> !Objects.equals(node.lastRecord().getChangeStatus(), ChangeMainStatusEnum.STASHED.getCode()))
.collect(Collectors.toCollection(LinkedList::new))
.peekLast();
}
/**
* 判断是否有大于当前版本的发布版本,该判断会包含被隐藏的版本记录
* @param version 版本
* @return true 含有 false 没有
*/
public boolean hasPublishedVersionAfterThisVersion(Integer version) {
return this.originModels
.stream()
.filter(cm -> Objects.nonNull(cm.getVersion()))
.filter(cm -> cm.getVersion() > version)
.anyMatch(cm -> Objects.equals(cm.getChangeStatus(), ChangeMainStatusEnum.PUBLISHED.getCode()));
}
}
版本依赖
场景:业务单据存在包含关系,且单据之间会互相影响。
假设决策模型下包含多个合同模型对象:
@Data
class MotionModel {
private String motionNo;
private String motionName;
private List<ContractModel> contractModels;
}
解决方案1,在父单据中冗余子的业务单据,缺点明显:
父单据版本与子单据版本的对应关系,只能通过子单据版本的 fromChangeId
决定,如果想找到当前版本父业务单据找到关联的子业务单据的对应版本,是无法做到的
解决方案2,父单据只存储自己的信息,通过版本id关联业务版本:
可以通过版本索引找到每个子业务单据的版本以及版本的全部内容
代码开发复杂度变高,保存父业务单据需要提前存储其关联的所有业务单据后,并将他们的id刷入到父业务单据中,才可以完成版本刷新操作
版本在序列化、返序列化时,需要做拆分、组装等操作,对性能有一定的影响
方式一:依赖索引信息存储到content,并使用一个特殊的对象表示
这种方式可以做到代码上的无感知:
查询业务模型是,会自动将所有的版本引用替换为完整的业务信息
也拥有一定的缺点:
如果子单据发生变更,需要更新父单据的变更content,更新时需要拿到父单据的全量信息(繁琐,mysql是这样,mongo应该可以少拿一些)
假设决策模型下包含多个业务模型对象:
@Data
class MotionModel {
private String motionNo;
private String motionName;
private List<ContractModel> contractModels;
}
在执行序列化时,需要将ContractModel的内容替换为引用id,也就是说,序列化后的内容如下:
{
"contractModels": [
{
"changeReferenceId": 2
},
{
"changeReferenceId": 3
}
]
}
而将上述json反序列化后,需要还原成完整的数据对象信息:
{
"contractModels": [
{
"changeReferenceId": 2,
"contractId": 100,
"contractCode": "xxx",
"contractName": "xxx"
},
{
"changeReferenceId": 3,
"contractId": 101,
"contractCode": "yyy",
"contractName": "yyy"
}
]
}
冗余业务信息,以实现目标数据定位
子业务更新后,可能需要刷新父业务中自己的版本id,比如需要刷新 cotnractId
为100
的数据的版本从2
变为 4
,由于父业务中指存储了引用id,他无法定位具体的更新位置的,故冗余change
的其他关键信息:businessId
和businessType
,用于定位具体的更新位置:
{
"contractModels": [
{
"changeReferenceId": 2,
"changeReferenceBusinessId": 100,
"changeReferenceBusinessType": "coantract"
},
{
"changeReferenceId": 3,
"changeReferenceBusinessId": 101,
"changeReferenceBusinessType": "coantract"
}
]
}
引用标识接口 ChangeReference
定义引用标识接口,所有需要做引用处理的业务模型类,都需要实现该接口:
public interface ChangeReference {
String CHANGE_REFERENCE_ID = "changeReferenceId";
void setChangeReferenceId(Long changeReferenceId);
Long getChangeReferenceId();
String CHANGE_REFERENCE_BUSINESS_ID = "changeReferenceBusinessId";
void setChangeReferenceBusinessId(Long changeReferenceBusinessId);
Long getChangeReferenceBusinessId();
String CHANGE_REFERENCE_BUSINESS_TYPE = "changeReferenceBusinessType";
void setChangeReferenceBusinessType(String changeReferenceBusinessType);
String getChangeReferenceBusinessType();
/**
* 是否是一个有效的版本引用
*/
default boolean isValid() {
return Objects.nonNull(this.getChangeReferenceId())
&& Objects.nonNull(this.getChangeReferenceBusinessId())
&& StringUtils.hasText(this.getChangeReferenceBusinessType());
}
}
还提供了一个基本实现类,用来表示一个没有任何业务信息的简单引用,该类适合只需要记录版本关系的情况:
@Data
public class SimpleChangeReference implements ChangeReference {
/**
* 关联版本id
*/
private Long changeReferenceId;
/**
* 关联版本的业务id
*/
private Long changeReferenceBusinessId;
/**
* 关联版本的业务类型
*/
private String changeReferenceBusinessType;
}
版本嵌套的层级限制
版本和版本之间可以相互关联是由前提得:
版本嵌套的层级不能超过三层,比如决策依赖业务 这是二层, 决策依赖业务、业务依赖土地信息,这是三层
使用序列化、返序列化以支持版本嵌套
在ChangeManager
中提供两个方法,分别用于序列化、反序列化:
// 序列化,并生成content
String serializeContent(Object obj);
// 返序列化content,生成业务model
<T> T deserializeContent(Class<T> clazz, String content);
如果有任何类实现了ChangeReference
接口,并且他引用是一个有效的引用信息,就认为这是一个有效的版本索引;那么在序列化时,只会存储版本信息,其他业务信息不会做存储;同样的,在反序列化时,如果目标类型是一个ChangeReference
类型,并且返序列化后的对象是一个有效的版本引用,那么就会通过反射的方式收集所有的引用id,批量查询content,然后将content反序列化为对应的类型。
嵌套版本的单据的各种操作
假设父业务单据为决策,子业务单据为业务,决策操作时会携带所有的业务信息,模拟他们的操作时的版本行为如下:
决策保存,决策含有完整的业务信息
// 先处理关联的业务
motionModel.getContractList().forEach(contractModel -> {
ChangeModel contractChange = new ChangeModel();
contractChange.setBusinessId(contractModel.getId());
contractChange.setBusinessType("contract");
contractChange.setContent(changeModel.seralize());
changeManager.stash(contractChange);
contractModel.setChangeReferenceId(contractChange.getId());
});
ChangeModel motionChange = createMotionChange(motionModel);
changeManager.stash(motioChange);
决策操作时不会携带所有的业务信息,他们是分开操作的,版本行为如下:
决策保存,决策只有业务的版本信息,不包含完整的业务信息
// 先创建一个决策
changeManager.stash(motionChange);
// ---------------------------------
// 添加一个业务
changeManager.stash(contractChange);
// 添加业务后,维护决策与业务的版本关系
motionModel.getContractList().add(new SimpleModel(contractChange.getId(), "contract", contractModel.getId()));
changeManager.stash(motionChange);
// ---------------------------------
// 修改一笔已存在的业务,刷新changeId
changeManager.stash(contractChange);
// 添加业务后,维护决策与业务的版本关系
motionModel.getContractList().refreshContract(contractModel.getId(), contractChange.getId());
changeManager.stash(motionChange);
序列化、返序列化代码实现样例(Jackson 与 反射)
序列化时,如果当前对象是一个有效的版本引用,那么序列化时,忽略该对象的其他所有字段,只序列化版本引用信息入库:
// 定义一个ChangeReference的专属类型的Jackson序列化器
private static final ObjectMapper mapper = new ObjectMapper();
static {
SimpleModule module = new SimpleModule();
module.addSerializer(ChangeReference.class, new ChangeReferenceSerializer());
mapper.registerModule(module);
}
private static class ChangeReferenceSerializer extends JsonSerializer<ChangeReference> {
@Override
public void serialize(ChangeReference changeReference, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if (Objects.nonNull(changeReference.getChangeReferenceId())) {
jsonGenerator.writeStartObject();
jsonGenerator.writeFieldName(CHANGE_REFERENCE_ID);
jsonGenerator.writeNumber(changeReference.getChangeReferenceId());
jsonGenerator.writeEndObject();
return;
}
jsonGenerator.writeStartObject();
// 使用反射获取目标对象的所有字段
Field[] fields = changeReference.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
String fieldName = field.getName();
Object fieldValue;
try {
fieldValue = field.get(changeReference);
} catch (IllegalAccessException e) {
fieldValue = null;
}
jsonGenerator.writeObjectField(fieldName, fieldValue);
}
jsonGenerator.writeEndObject();
}
}
public String serializeContent(Object obj) {
Assert.notNull(obj, "change目标对象不可为空");
try {
return mapper.writeValueAsString(obj);
} catch (IOException e) {
log.error("序列化content出现错误 {}", e.getMessage(), e);
return null;
}
}
反序列化时,在Jackson处理后,使用反射 + 递归的方式获取所有的 changeReferenceId
,然后从库中查询到所有的版本信息,并填充到目标对象中:
public <T> T deserializeContent(Class<T> clazz, String content) {
Assert.notNull(clazz, "class不可为空");
Assert.notNull(content, "content不可为空");
T result;
try {
result = mapper.readValue(content, clazz);
} catch (IOException e) {
log.error("反序列化content出现错误 {}", e.getMessage(), e);
return null;
}
// 递归获取所有实现了ChangeReference接口的字段,取出所有的changeReferenceId
Set<Long> ids = getAllChangeReferenceIds(result);
log.info("反序列化后所有的关联changeId为 {}", JSON.toJSONString(ids));
if (ids.isEmpty()) {
return result;
}
Map<Long, Change> changeMap = changeMapper.selectList(Wrappers.lambdaQuery(Change.class)
.in(Change::getId, ids)
)
.stream()
.collect(Collectors.toMap(Change::getId, Function.identity(), (o, o2) -> o));
if (changeMap.isEmpty()) {
return result;
}
fillModelFromChangeMap(result, changeMap);
return result;
}
private Set<Long> getAllChangeReferenceIds(Object object) {
Set<Long> referenceIds = new HashSet<>();
if (Objects.isNull(object)) {
return referenceIds;
}
if (Collection.class.isAssignableFrom(object.getClass())) {
((Collection<?>) object).stream().map(this::getAllChangeReferenceIds).forEach(referenceIds::addAll);
return referenceIds;
}
if (Map.class.isAssignableFrom(object.getClass())) {
((Map<?, ?>) object).values().stream().map(this::getAllChangeReferenceIds).forEach(referenceIds::addAll);
return referenceIds;
}
// 如果既不是map,也不是集合,且他又不是com.rhdk下的类
if (notRhdkClass(object.getClass())) {
return referenceIds;
}
// 如果本身就是ChangeReference类型,添加 referenceId
// 如果类型为SimpleChangeReference ,说明model对象不需要业务的完整信息,只是记录关联的版本
if (ChangeReference.class.isAssignableFrom(object.getClass()) && SimpleChangeReference.class != object.getClass()) {
Object id = ReflectUtil.getFieldValue(object, CHANGE_REFERENCE_ID);
if (Objects.nonNull(id)) {
referenceIds.add((Long) id);
}
}
// 继续递归处理其他字段值,获取处理他的其他所有字段
Object[] fieldsValue = ReflectUtil.getFieldsValue(object);
if (Objects.nonNull(fieldsValue)) {
Arrays.stream(fieldsValue)
.map(this::getAllChangeReferenceIds)
.forEach(referenceIds::addAll);
}
return referenceIds;
}
/**
* 将变更索引id,填充为model对象
*
* @param object 目标业务对象
* @param changeMap 变更信息map
* @param <T> 业务模型类型
*/
private <T> void fillModelFromChangeMap(T object, Map<Long, Change> changeMap) {
if (Objects.isNull(object)) {
return;
}
if (Collection.class.isAssignableFrom(object.getClass())) {
((Collection<?>) object).forEach(o -> this.fillModelFromChangeMap(o, changeMap));
return;
}
if (Map.class.isAssignableFrom(object.getClass())) {
((Map<?, ?>) object).values().forEach(o -> this.fillModelFromChangeMap(o, changeMap));
return;
}
// 如果既不是map,也不是集合,且他又不是com.rhdk下的类
if (notRhdkClass(object.getClass())) {
return;
}
// 如果本身就是ChangeReference类型,并且referenceId不为null,就填充业务信息
if (ChangeReference.class.isAssignableFrom(object.getClass())) {
Long id = (Long) ReflectUtil.getFieldValue(object, CHANGE_REFERENCE_ID);
Change targetChange = changeMap.get(id);
if (Objects.nonNull(id) && Objects.nonNull(targetChange) && StringUtils.isNotBlank(targetChange.getContent())) {
// 将targetChange转换为业务对象
Object model = JacksonUtil.toObj(targetChange.getContent(), object.getClass());
AssertUtil.notNull(model, "转换业务model出现错误,请确认content内容是否正确,目标id=" + id);
// 填充所有的字段,不忽略null值,忽略changeId字段
BeanUtil.copyProperties(model,
object,
CopyOptions.create().setIgnoreNullValue(false).setIgnoreProperties(ChangeReference::getChangeReferenceId)
);
}
}
// 继续递归处理所有字段
Object[] fieldsValue = ReflectUtil.getFieldsValue(object);
if (Objects.nonNull(fieldsValue)) {
Arrays.stream(fieldsValue).forEach(o -> fillModelFromChangeMap(o, changeMap));
}
}
private boolean notRhdkClass(Class<?> clazz) {
Package objectPackage = clazz.getPackage();
if (null == objectPackage) {
return true;
} else {
String objectPackageName = objectPackage.getName();
return !objectPackageName.startsWith("com.rhdk.");
}
}
方式二:依赖索引信息单独存储到一个字段中
在t_change
表中增加字段 relationship
,用以记录当前版本关联的所有版本:
他的类型是一个json类型,你可以设计该类型为任意一种结构,比如设计为一个id数组:
也可以这样:
{
"contractModel": [1, 2, 3, 4, 5]
}
这种方式的缺点为:
在替换子业务(业务)的版本,需要在父业务(决策)版本维度操作,将所有的id生成好后再重新刷入
如果是数组方式,业务端还要判断每个索引的id是属于什么类型的业务的,并对他们进行分组填入
从父单据可以很容易的通过relationship
找到关联子单据的对应的版本,但是从子单据,很难获取父单据的版本。所以数组方式所表现的数据场景有限,可以使用自定义的json结构来存储表示,比如:
{
"motionId": 123,
"contractModels" : [
123, 124, 125, "自己"
]
}
具体处理方式由业务端决定。