> For the complete documentation index, see [llms.txt](https://yangsx95.gitbook.io/notes/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://yangsx95.gitbook.io/notes/architect/dai-ma-she-ji-he-dai-ma-zhong-gou/li-shi-yu-ban-ben.md).

# 历史与版本

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

## 使用版本号

版本号是一种常见的版本设计方案，就是在要进行历史数据保留的表上面增加一个版本号字段，该字段可以是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`两个字段可以找到该笔业务单据的所有版本历史。

其中单条版本的内容如下：

```java
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;
}
```

### 版本的状态

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

![版本状态机图](/files/5vvkUSvUcznlnCrkQ6IF)

注意：

1. 所有动作的执行都会校验当前change的状态，只有指定状态下才可做指定的操作
2. stash 暂存数据时，如果暂存数据是新的一条，那么会自动生成编号（唯一），并且生成version

### 版本的额外信息存储 Metadata

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

* 版本的生成来源是手动还是自动
* 版本是否需要隐藏
* 版本的变更等级
* 版本的提交时间
* 版本的提交人

等等等。

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

#### 业务单据会签状态

![业务单据会签状态](/files/Xyh6DMTGiMJB0um8WlN0)

1. 手动保存

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

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

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

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

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

   ```java
   changeManager.publish(change);
   ```

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

#### 业务单据变更状态

![业务单据变更状态](/files/SdbUprIQ3rwCaxPCjmnU)

1. 变更保存

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

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

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

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

   ```java
   changeManager.publish(change);
   ```

#### 变更与审批的关系

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

```java
// 审批流代码
String workflowCode;
```

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

审批的发起是使用`t_change`的`id` + `workflowCode` 作为唯一标识；审批的`workflowCode`在审批一发起的时候就进行设置，后续无论进行任何操作，都不做更新。

```java
change.getMetadata().setWorkflowCode("xxxxWorkflow");
changeManager.commit(change);
startProcess(changeId, workflowCode, xxxx);
```

#### 变更等级

在业务发起变更时，根据变更内容的不同，需要决定不同的审批流；变更等级分为四种：

```txt
0级变更 无变更，提示没有做出修改，不允许变更
1级变更 走完整的审批流程
2级变更 走简略的审批流程
3级变更 不走审批流程
```

变更等级的计算是当前版本与上个`published`的版本相对比，以找出对应的等级。找出等级后，为了避免实时对比对性能有所损耗，会将变更等级存储在标签中：

```java
// 变更等级在版本一开始暂存的时候就会计算好，并填入到metadata中
// 每次暂存都会重新计算一次
change.getMetadata().setChangeLevel(level);
changeManager.stash(change);
```

#### 业务自动新增和手动新增

决策在审批通过后，会生成关联的业务信息，自动生成的业务，即使是未会签也需要生成版本：

```java
// 自动新增一个业务
contractManager.submitAuto(contract);
// 设置标签信息
changeModel.getMetadata()
           .setBusinessOperateStage(BusinessOperateStageEnum.GENERATE)
           .setGenerateSource(BusinessGenerateSource.AUTO);
```

设置业务阶段为生成阶段，是为了区分该操作对应的业务操作阶段，以支持变更历史以及审批履历的标签信息，目前将阶段分为三个阶段：

```java
GENERATE("generate", "生成阶段"),
CHANGE("change", "变更阶段"),
TERMINAL("terminal", "终止阶段"),
```

设置生成来源，是为了标记业务的创建来源是什么，有手动创建与自动生成两种：

```java
// 由其他业务自动生成的，自动生成
AUTO("auto", "自动生成"),
MANUAL("manual", "手动创建"),
```

手动创建的业务会在暂存的时候设置生成来源为手动创建。

#### 版本来源

```java
private Long fromOtherChangeId;
```

一个版本的升级，可能是他本身发生变化导致的，也可能是由其他版本影响导致的。如果是受其他版本影响所导致的，需要设置来源的`changeId`。

比如，当决策发生变更时，其关联的业务会发生变更，这时候就需要记录这个版本的变更来源版本的changeId，已确保清楚每个变更的来源。

在决策影响点到业务处，会设置决策当前的`changeId`到业务影响版本。

#### 作废来源

```java
private BusinessVoidSourceEnum voidSource;

UNDO("undo", "撤销"),
REFUSED("refused", "拒绝"),
```

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

1. 无论是审批撤回还是审批拒绝，change的状态都会变为`void`已作废状态。为了区分作废倒是是因为什么操作作废的。
2. 撤销的版本在版本历史中隐藏，故需要此字段进行标记。

#### 撤销与状态回退

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

实现方式为，在版本生命周期结束时，记录业务的变更状态到metadata中：

```java
/**
 * 业务变更状态(仅业务使用)
 */
private ContractChangeStatusEnum contractChangeStatus;
```

```java
// 审批通过、审批拒绝、审批撤回时，会设置此状态
changeModel.getMetadata()
                .setVoidSource(BusinessVoidSourceEnum.REFUSED)
                .setMotionChangeStatus(BaseEnum.getEnum(motionModel.getChangeStatus(), MotionChangeStatusEnum.class));
changeManager.voids(changeModel);
```

撤销时，会查询上个版本，拿到状态与审批状态，并回退：

```java
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隐藏标记：

```java
/**
 * 隐藏标记，如果该字段不为空，代表该条变更记录需要隐藏
 */
private YesOrNoEnum hiddenFlag = YesOrNoEnum.NO;
```

隐藏标记不会记录在版本历史中，具体参考`ChangeByStageHistoryModel`。

#### 版本历史核心类`ChangeByStageHistoryModel`

`ChangeByStageHistoryModel`是一个以阶段为主要标记的版本历史帮助容器类：

![图 1](/files/YX9tarTrt6oogg2zoovq)

1. 历史记录是一个链表，链表以`version`字段正序排序
2. 一个节点中可能包含多个版本，版本记录是一个`TreeSet`，也已字段`version`正序排序
3. 节点的状态是由节点内的版本决定的：
   1. changeId、changeCode、version、changeStatus，取节点内最大版本的信息
   2. workflowCode则是从所有节点内的版本中取有workflowCode的第一条
4. position用于确定版本节点在历史中的位置，比如如果Node是位于第一位置的，列表则不显示版本编号
5. 变更次数：统计变更阶段的Node数量
6. 拥有隐藏标记的版本，不会记录到版本历史中。但是会存在于 `originModels`中，因为隐藏版本有可能需要使用，比如查询上个生效版本应该包含隐藏版本。

```java
/**
 * 变更历史业务对象
 *
 * @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()));
    }

}
```

### 版本依赖

场景：业务单据存在包含关系，且单据之间会互相影响。

假设决策模型下包含多个合同模型对象：

```java
@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应该可以少拿一些）

假设决策模型下包含多个业务模型对象：

```java
@Data
class MotionModel {
    private String motionNo;
    private String motionName;
    private List<ContractModel> contractModels;
}
```

在执行序列化时，需要将ContractModel的内容替换为引用id，也就是说，序列化后的内容如下：

```json
{
   "contractModels": [
      {
         "changeReferenceId": 2
      },
      {
         "changeReferenceId": 3
      }
   ]
}
```

而将上述json反序列化后，需要还原成完整的数据对象信息：

```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`，用于定位具体的更新位置：

```java
{
   "contractModels": [
      {
         "changeReferenceId": 2,
         "changeReferenceBusinessId": 100,
         "changeReferenceBusinessType": "coantract"
      },
      {
        "changeReferenceId": 3,
        "changeReferenceBusinessId": 101,
        "changeReferenceBusinessType": "coantract"
      }
   ]
}
```

**引用标识接口 `ChangeReference`**

定义引用标识接口，所有需要做引用处理的业务模型类，都需要实现该接口：

```java
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());
    }
}
```

还提供了一个基本实现类，用来表示一个没有任何业务信息的简单引用，该类适合只需要记录版本关系的情况：

```java
@Data
public class SimpleChangeReference implements ChangeReference {

    /**
     * 关联版本id
     */
    private Long changeReferenceId;

    /**
     * 关联版本的业务id
     */
    private Long changeReferenceBusinessId;

    /**
     * 关联版本的业务类型
     */
    private String changeReferenceBusinessType;
}
```

**版本嵌套的层级限制**

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

1. 版本与版本的依赖不能出现循环依赖
2. 版本嵌套的层级不能超过三层，比如决策依赖业务 这是二层， 决策依赖业务、业务依赖土地信息，这是三层

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

在`ChangeManager`中提供两个方法，分别用于序列化、反序列化：

```java
// 序列化，并生成content
String serializeContent(Object obj);

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

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

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

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

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

   ```java
   // 先处理关联的业务
   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. 决策保存，决策只有业务的版本信息，不包含完整的业务信息

   ```java
   // 先创建一个决策
   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 与 反射）**

序列化时，如果当前对象是一个有效的版本引用，那么序列化时，忽略该对象的其他所有字段，只序列化版本引用信息入库：

```java
// 定义一个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`，然后从库中查询到所有的版本信息，并填充到目标对象中：

```java
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数组：

```json
[1,2,3,4,5]
```

也可以这样：

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

这种方式的缺点为：

1. 业务端需要自己将索引信息填充到业务信息中
2. 在替换子业务（业务）的版本，需要在父业务（决策）版本维度操作，将所有的id生成好后再重新刷入
3. 如果是数组方式，业务端还要判断每个索引的id是属于什么类型的业务的，并对他们进行分组填入
4. 从父单据可以很容易的通过`relationship`找到关联子单据的对应的版本，但是从子单据，很难获取父单据的版本。所以数组方式所表现的数据场景有限，可以使用自定义的json结构来存储表示，比如：

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

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