历史与版本

希望目标实体以及数据可以留存下来,并且恢复到指定的版本。

使用版本号

版本号是一种常见的版本设计方案,就是在要进行历史数据保留的表上面增加一个版本号字段,该字段可以是DateTime类型,也可以是int类型,每进行数据操作时,都是创建一个新的版本,版本是只增不减的,所以只需要拿到最大一个版本号,就能得到最新的业务数据。

版本号除了能够用于留存历史数据外,还有一个功能就是避免并发编辑操作。比如我们有一个对象A,当前的版本是1,两个用户同时打开了该对象的编辑页面,进行数据更改。先是甲用户提交更改,这个时候系统把对象的ID和版本进行查询,发现要修改的数据最新版本是1,所以成功修改,保存了对象A的新版本2。这个时候用户乙也提交了修改。系统把对象的ID和版本1进行查询,发现要修改的数据最新版本是2,不符合要求,所以拒绝用户乙的修改。用户乙只有刷新界面,拿到最新的版本2,再进行修改。

ID单号金额版本号

1

EXP123

100

1

2

EXP123

120

2

但是,使用这种方式也有很明显的缺点:

  1. 历史版本信息与当前版本信息放在一张表中,容易导致表数据过大。解决办法:

    1. 限制存储历史版本的数量,比如一个实体最多存储10条历史记录,如果超过10条,删除最老的版本,再新增一个新的版本;或者直接更新最老的版本为新版本

    2. 对表中的数据进行分表操作

  2. 查询列表、或者是分页信息的代码将会变得很难写,导致代码很难维护

    1. 增加标记字段(newtest),标记当前条目是最新的版本,在查询时增加这个条件逻辑

  3. 没法设置中间态,某些版本可能需要先入库,但是并不是立即生效的,比如实体处于审批中的状态,使用这种方式没法区分生效失效的情况

    1. 使用业务状态判断,比如业务实体的审批状态,如果处于审批中,获取时就将这个条目排除

    2. 单独增加增加一个状态,用以表示该条目是否生效

使用生效时间,失效时间

保存历史数据的第二办法是使用生效失效时间来表示一个版本。要进行历史数据记录的表增加“生效时间”“失效时间”两个字段,两个字段不允许为空。对于刚创建的数据,生效时间是创建该数据的时间,失效时间是9999-12-31。现在对这条数据进行了修改,那么我们只需要将当前时间设置为上一个版本的失效时间,同时创建一条新数据,生效时间是当前时间,失效时间是9999-12-31即可。

ID单号金额生效时间失效时间

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

缺点:

  1. 历史版本信息与当前版本信息放在一张表中,容易导致表数据过大

  2. 查询时需要带着生效时间以及失效时间判断

  3. 无法直观知晓历史版本有多少个

使用版本历史表

使用历史表其实就是建立完全相同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;
}

版本的状态

一个版本也有他的生命周期,版本在创建后,如果发生了不同的操作,那么版本的状态转换也会发生不同,在遇到的实际项目设计中,版本的状态机图如下:

注意:

  1. 所有动作的执行都会校验当前change的状态,只有指定状态下才可做指定的操作

  2. stash 暂存数据时,如果暂存数据是新的一条,那么会自动生成编号(唯一),并且生成version

版本的额外信息存储 Metadata

要想支持复杂的版本记录场景,单据记录版本历史是无法满足所有的需求的,故增加一个Metadata字段,他的类型与content字段类似,可以是任意结构。但是与content内容不同的是,metadata用来记录与版本本身相关的数据,比如:

  • 版本的生成来源是手动还是自动

  • 版本是否需要隐藏

  • 版本的变更等级

  • 版本的提交时间

  • 版本的提交人

等等等。

示例:业务单据实现版本变更

业务单据会签状态

  1. 手动保存

    // 暂存用户提交内容,如果是第一次就会插入,否则更新暂存的版本
    changeManager.stash(change);
  2. 自动新增

    // 自动新增的数据可能来自其他单据,故需要做版本记录
    changeManager.stash(change);
    changeManager.commit(change);
    changeManager.publish(change);
  3. 删除(略,只有为会签数据可被删除)

  4. 提交

    changeManager.commit(commit);
  5. 审批撤回

    changeManager.voids(change);
    changeManager.stash(newChange);
  6. 审批拒绝

    changeManager.voids(change);
  7. 审批通过

    changeManager.publish(change);

业务的每个操作都会进行版本记录。

业务单据变更状态

  1. 变更保存

    changeManager.stash(change);
  2. 变更提交

    changeManager.commit(change);
  3. 变更审批撤销

    changeManager.voids(change);
    changeManager.stash(newChange);
  4. 变更审批拒绝

    changeManager.voids(change);
  5. 变更审批通过

    changeManager.publish(change);

变更与审批的关系

业务进行会签、变更操作时,有可能发生审批。如果该版本经过了审批,那么就会在metadata中设置一个标签:

// 审批流代码
String workflowCode;

如果workflowCode不为空,那么代表该版本经过了审批,有审批履历。

审批的发起是使用t_changeid + 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", "拒绝"),

作废来源,代表这个版本被作废的原因,设置改元数据字段的主要意图有:

  1. 无论是审批撤回还是审批拒绝,change的状态都会变为void已作废状态。为了区分作废倒是是因为什么操作作废的。

  2. 撤销的版本在版本历史中隐藏,故需要此字段进行标记。

撤销与状态回退

需求要求在撤销后,重新生成暂存记录,并且业务的变更状态、审批状态,要回到上个状态中;比如,上个变更状态为变更拒绝,审批状态为已拒绝,用户发起变更后,再撤销,状态需要回到这两个状态中。

实现方式为,在版本生命周期结束时,记录业务的变更状态到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是一个以阶段为主要标记的版本历史帮助容器类:

  1. 历史记录是一个链表,链表以version字段正序排序

  2. 一个节点中可能包含多个版本,版本记录是一个TreeSet,也已字段version正序排序

  3. 节点的状态是由节点内的版本决定的:

    1. changeId、changeCode、version、changeStatus,取节点内最大版本的信息

    2. workflowCode则是从所有节点内的版本中取有workflowCode的第一条

  4. position用于确定版本节点在历史中的位置,比如如果Node是位于第一位置的,列表则不显示版本编号

  5. 变更次数:统计变更阶段的Node数量

  6. 拥有隐藏标记的版本,不会记录到版本历史中。但是会存在于 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,在父单据中冗余子的业务单据,缺点明显:

  1. 需要做数据刷新同步

  2. 冗余数据造成额外磁盘以及内存的空间消耗

  3. 父单据版本与子单据版本的对应关系,只能通过子单据版本的 fromChangeId 决定,如果想找到当前版本父业务单据找到关联的子业务单据的对应版本,是无法做到的

解决方案2,父单据只存储自己的信息,通过版本id关联业务版本:

  1. 单据版本信息解耦,不需要做数据刷新同步

  2. 不会产生冗余数据

  3. 可以通过版本索引找到每个子业务单据的版本以及版本的全部内容

  4. 代码开发复杂度变高,保存父业务单据需要提前存储其关联的所有业务单据后,并将他们的id刷入到父业务单据中,才可以完成版本刷新操作

  5. 版本在序列化、返序列化时,需要做拆分、组装等操作,对性能有一定的影响

方式一:依赖索引信息存储到content,并使用一个特殊的对象表示

这种方式可以做到代码上的无感知:

  1. 定义业务模型时,定义业务的所有字段

  2. 查询业务模型是,会自动将所有的版本引用替换为完整的业务信息

也拥有一定的缺点:

  1. 版本关联关系被耦合到业务模型中(问题不大)

  2. 如果子单据发生变更,需要更新父单据的变更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,比如需要刷新 cotnractId100的数据的版本从2变为 4,由于父业务中指存储了引用id,他无法定位具体的更新位置的,故冗余change的其他关键信息:businessIdbusinessType,用于定位具体的更新位置:

{
   "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;
}

版本嵌套的层级限制

版本和版本之间可以相互关联是由前提得:

  1. 版本与版本的依赖不能出现循环依赖

  2. 版本嵌套的层级不能超过三层,比如决策依赖业务 这是二层, 决策依赖业务、业务依赖土地信息,这是三层

使用序列化、返序列化以支持版本嵌套

ChangeManager中提供两个方法,分别用于序列化、反序列化:

// 序列化,并生成content
String serializeContent(Object obj);

// 返序列化content,生成业务model
<T> T deserializeContent(Class<T> clazz, String content);

如果有任何类实现了ChangeReference接口,并且他引用是一个有效的引用信息,就认为这是一个有效的版本索引;那么在序列化时,只会存储版本信息,其他业务信息不会做存储;同样的,在反序列化时,如果目标类型是一个ChangeReference类型,并且返序列化后的对象是一个有效的版本引用,那么就会通过反射的方式收集所有的引用id,批量查询content,然后将content反序列化为对应的类型。

嵌套版本的单据的各种操作

假设父业务单据为决策,子业务单据为业务,决策操作时会携带所有的业务信息,模拟他们的操作时的版本行为如下:

  1. 决策保存,决策含有完整的业务信息

    // 先处理关联的业务
    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);

决策操作时不会携带所有的业务信息,他们是分开操作的,版本行为如下:

  1. 决策保存,决策只有业务的版本信息,不包含完整的业务信息

    // 先创建一个决策
    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,用以记录当前版本关联的所有版本:

字段名字段类型

references

json

他的类型是一个json类型,你可以设计该类型为任意一种结构,比如设计为一个id数组:

[1,2,3,4,5]

也可以这样:

{
    "contractModel": [1, 2, 3, 4, 5]
}

这种方式的缺点为:

  1. 业务端需要自己将索引信息填充到业务信息中

  2. 在替换子业务(业务)的版本,需要在父业务(决策)版本维度操作,将所有的id生成好后再重新刷入

  3. 如果是数组方式,业务端还要判断每个索引的id是属于什么类型的业务的,并对他们进行分组填入

  4. 从父单据可以很容易的通过relationship找到关联子单据的对应的版本,但是从子单据,很难获取父单据的版本。所以数组方式所表现的数据场景有限,可以使用自定义的json结构来存储表示,比如:

    {
       "motionId": 123,
       "contractModels" : [
          123, 124, 125, "自己"
       ]
    }

    具体处理方式由业务端决定。

最后更新于