Flowable 中的网关、流程变量以及历史流程

news2024/11/25 22:47:05

今天这篇文章,松哥和大家梳理一下 Flowable 中的网关、流程变量以及历史流程的玩法。

1. 三大网关

Flowable 中网关类型其实也不少,常见的主要有三种类型,分别是:

  1. 排他网关
  2. 并行网关
  3. 包容网关

这三个里边最常用的当然就是排他网关了,今天松哥就来和小伙伴们聊一聊这三种网关,一起来体验一把这三种网关各自的特征。

1.1. 排他网关

首先就是排他网关了,这个也叫互斥网关,长得像下图这样:

排他网关可以有 N 个入口,但是只有一个有效出口。

松哥举一个例子:

假设我有一个请假流程,请假 1 天,组长审批,请假小于 3 天,项目经理审批,请假大于 3 天,总监审批,据此,我们可以绘制如下流程图:

在这个流程图中,当流程从排他网关出来的时候,我们设置一个变量,根据变量的值,来决定下一个走哪一个 Task,例如组长审批,我们做如下配置:

这个流条件表示当 days 这个变量的值小于等于 1 的时候,就会进入到组长审批这个 Task。

按照类似的方式,我们来设置经理审批:

最后,总监审批的条件如下:

最终,我们来看下这个流程对应的 XML 文件,如下:

  <process id="demo01" name="测试流程" isExecutable="true">
    <documentation>测试流程</documentation>
    <startEvent id="startEvent1" flowable:formFieldValidation="true"></startEvent>
    <exclusiveGateway id="sid-C4E389D6-C507-4B8E-8469-2288AA5B44A5"></exclusiveGateway>
    <sequenceFlow id="sid-DF97CC8B-3AD5-447D-AE67-1082CAB7B189" sourceRef="startEvent1" targetRef="sid-C4E389D6-C507-4B8E-8469-2288AA5B44A5"></sequenceFlow>
    <userTask id="sid-B4CD08AF-52B5-44F2-AC45-B2F5E154A5F0" name="组长审批" flowable:formFieldValidation="true"></userTask>
    <userTask id="sid-07B7951C-4E76-4639-989C-407C610C5BA8" name="经理审批" flowable:formFieldValidation="true"></userTask>
    <userTask id="sid-1A81B40F-D8D4-4158-B0B9-26DB8FB7DD2E" name="总监审批" flowable:formFieldValidation="true"></userTask>
    <endEvent id="sid-0F56FE56-1A8C-4B47-8F0D-196700DDF7B8"></endEvent>
    <sequenceFlow id="sid-E4B4B580-F078-4BB9-B5D3-966E80737C4C" sourceRef="sid-B4CD08AF-52B5-44F2-AC45-B2F5E154A5F0" targetRef="sid-0F56FE56-1A8C-4B47-8F0D-196700DDF7B8"></sequenceFlow>
    <endEvent id="sid-F05670CB-A8F4-44A3-B53D-46CFB6F65581"></endEvent>
    <sequenceFlow id="sid-3EC62E5D-ACDA-480E-93B4-C24D8F6E9042" sourceRef="sid-07B7951C-4E76-4639-989C-407C610C5BA8" targetRef="sid-F05670CB-A8F4-44A3-B53D-46CFB6F65581"></sequenceFlow>
    <endEvent id="sid-52711414-1769-4EC3-9AE5-6BA426123095"></endEvent>
    <sequenceFlow id="sid-C81500B2-D1EA-429F-8402-A3D8C8CA0E29" sourceRef="sid-1A81B40F-D8D4-4158-B0B9-26DB8FB7DD2E" targetRef="sid-52711414-1769-4EC3-9AE5-6BA426123095"></sequenceFlow>
    <sequenceFlow id="sid-807C7B79-4AFA-4525-847F-4D0FE1C0F0F3" name="小于1天" sourceRef="sid-C4E389D6-C507-4B8E-8469-2288AA5B44A5" targetRef="sid-B4CD08AF-52B5-44F2-AC45-B2F5E154A5F0">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${days<=1}]]></conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="sid-3D3DF742-BF47-4536-9EE9-747CD284A1BA" name="1-3天" sourceRef="sid-C4E389D6-C507-4B8E-8469-2288AA5B44A5" targetRef="sid-07B7951C-4E76-4639-989C-407C610C5BA8">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${days>1 && days<=3}]]></conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="sid-2AD41E43-AFEC-47A1-B8D1-0B4299434BF8" name="大于3天" sourceRef="sid-C4E389D6-C507-4B8E-8469-2288AA5B44A5" targetRef="sid-1A81B40F-D8D4-4158-B0B9-26DB8FB7DD2E">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${days>3}]]></conditionExpression>
    </sequenceFlow>
  </process>
复制代码

可以看到,在 sequenceFlow 标签中,有一个 conditionExpression 标签,这个标签的内容就是具体的条件了。

现在,我们部署一下这个流程,然后按照如下方式来启动:

@Test
void test01() {
    Map<String, Object> variables = new HashMap<>();
    variables.put("days", 3);
    ProcessInstance pi = runtimeService.startProcessInstanceByKey("demo01", variables);
    logger.info("id:{},activityId:{}", pi.getId(), pi.getActivityId());
}
复制代码

注意,这个启动的时候,传入一个 days 变量,系统将来会根据这个变量来决定这个流程要走到哪一个 Task。流程启动成功之后,我们去观察 ACT_RU_TASK 表,就可以看到流程的执行是否和我们所预想的一致。

1.2. 并行网关

并行网关,从名字上大概也能看出来,这种网关一般用在并行任务上,并行网关如下图:

并行网关一般是成对出现的,一个出现的并行网关用来分流,第二个出现的并行网关用来聚合。

我画一个简单的并行网关的例子,如下图:

小伙伴们看到,这是一个简化的生产笔记本的流程图,当屏幕和键盘都生产好之后,再进行组装,整个流程图中存在两个并行网关(成对出现)。

在这个流程图中,连接线上是不需要设置条件的(不同于拍他网关),这里即使你设置了条件,这个条件也是不会生效的。

我们来看下这个并行网关流程图对应的 XML 文件,如下:

<process id="demo01" name="测试流程" isExecutable="true">
  <documentation>测试流程</documentation>
  <startEvent id="sid-4F7F76BA-526A-4D8C-B45A-02FC1C56CA47" flowable:formFieldValidation="true"></startEvent>
  <sequenceFlow id="sid-11130848-EA1F-458A-A45D-49CBC49428C8" sourceRef="sid-4F7F76BA-526A-4D8C-B45A-02FC1C56CA47" targetRef="sid-6D01D4BE-C475-4270-8745-92752EA2C038"></sequenceFlow>
  <parallelGateway id="sid-6D01D4BE-C475-4270-8745-92752EA2C038"></parallelGateway>
  <userTask id="sid-54DD6BFA-FE6C-4DE7-9038-3DEEAF85002C" name="生产屏幕" flowable:assignee="zhangsan" flowable:formFieldValidation="true">
    <extensionElements>
      <modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
    </extensionElements>
  </userTask>
  <sequenceFlow id="sid-8DD3383C-45D1-4EAF-9A22-702A5B9D0869" sourceRef="sid-6D01D4BE-C475-4270-8745-92752EA2C038" targetRef="sid-54DD6BFA-FE6C-4DE7-9038-3DEEAF85002C"></sequenceFlow>
  <userTask id="sid-7797ED55-155F-4D17-8EA5-DE40434C421B" name="生产键盘" flowable:assignee="lisi" flowable:formFieldValidation="true">
    <extensionElements>
      <modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
    </extensionElements>
  </userTask>
  <sequenceFlow id="sid-6E992E8B-CF71-411D-B537-42FEDF4F4209" sourceRef="sid-6D01D4BE-C475-4270-8745-92752EA2C038" targetRef="sid-7797ED55-155F-4D17-8EA5-DE40434C421B"></sequenceFlow>
  <sequenceFlow id="sid-8DCA9516-FFED-4781-9ACC-530DC6E63755" sourceRef="sid-7797ED55-155F-4D17-8EA5-DE40434C421B" targetRef="sid-98D3C336-9AD9-4964-9CCB-496C850EE40F"></sequenceFlow>
  <sequenceFlow id="sid-EE80AE42-D021-4B9F-A91E-BD37C512EE65" sourceRef="sid-54DD6BFA-FE6C-4DE7-9038-3DEEAF85002C" targetRef="sid-98D3C336-9AD9-4964-9CCB-496C850EE40F"></sequenceFlow>
  <userTask id="sid-4FFE361A-E2AF-4481-BACF-1E618E8C4A26" name="组装" flowable:assignee="javaboy" flowable:formFieldValidation="true">
    <extensionElements>
      <modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
    </extensionElements>
  </userTask>
  <sequenceFlow id="sid-8CABC6E8-E36A-4814-B897-817D4A9F231C" sourceRef="sid-98D3C336-9AD9-4964-9CCB-496C850EE40F" targetRef="sid-4FFE361A-E2AF-4481-BACF-1E618E8C4A26"></sequenceFlow>
  <endEvent id="sid-BF02170B-8138-4867-AE01-E3B29505183D"></endEvent>
  <sequenceFlow id="sid-F72B2A15-913F-436E-8AD7-6A6FB190E197" sourceRef="sid-4FFE361A-E2AF-4481-BACF-1E618E8C4A26" targetRef="sid-BF02170B-8138-4867-AE01-E3B29505183D"></sequenceFlow>
  <parallelGateway id="sid-98D3C336-9AD9-4964-9CCB-496C850EE40F"></parallelGateway>
</process>
复制代码

现在我们把这个流程部署并启动。

流程启动成功之后,我们发现在 ACT_RU_TASK 表中有两个需要执行的 Task,如下图:

这两个 Task,如果只执行掉其中一个,那么还剩下另外一个 Task,如果两个都执行了,那么你就会看到一个新的 Task,如下图(两个并行任务执行完成后,进入到下一个任务):

好啦,这就是并行网关。

1.3. 包容网关

包容网关,有时候也叫相容网关、兼容网关等,如下图:

包容谁呢?包容排他网关和并行网关。也就是说,这种包容网关可以根据实际条件转为排他网关或者并行网关。

举个栗子:

假如说报销金额大于 500,zhangsan 审批,报销金额大于 1000,则需要 zhangsan 和 lisi 同时审批,且 zhangsan 和 lisi 审批无先后顺序。

据此,我绘制如下流程图:

在报销金额大于 500 上设置如下条件:

大于 1000 上设置如下条件:

接下来我们来部署好这个流程。

部署好之后,我们首先来启动流程,第一次启动的时候,我们设置报销金额为 666,如下:

@Test
void test01() {
    Map<String, Object> variables = new HashMap<>();
    variables.put("money", 666);
    ProcessInstance pi = runtimeService.startProcessInstanceByKey("demo01", variables);
    logger.info("id:{},activityId:{}", pi.getId(), pi.getActivityId());
}
复制代码

流程启动之后,我们在 ACT_RU_TASK 表中可以看到,该 zhangsan 审批了,如下:

zhangsan 审批之后,就是 wangwu 审批了,我就不演示了。

假设我们启动流程的时候,报销金额为 2000,如下:

@Test
void test01() {
    Map<String, Object> variables = new HashMap<>();
    variables.put("money", 2000);
    ProcessInstance pi = runtimeService.startProcessInstanceByKey("demo01", variables);
    logger.info("id:{},activityId:{}", pi.getId(), pi.getActivityId());
}
复制代码

那么此时你就会看到,在 ACT_RU_TASK 表中,出现了两条记录,分别是 zhangsan 审批和 lisi 审批,此时这两个审批就是一个并行任务了:

接下来就按并行任务的模式来,这两个人都审批了,才会进入到 wangwu 审批。

这就是兼容网关的特点,即根据实际情况,会变成排他网关或者并行网关。

好啦,三种常见的网关就和小伙伴们分享完啦,感兴趣的小伙伴赶紧试一试吧~

2. 四种变量设置方式

[TOC]

在之前的文章中,松哥也有和小伙伴们使用过流程变量,然而没有和大家系统的梳理过流程变量的具体玩法以及它对应的数据表详情,今天我们就来看看 Flowable 中流程变量的详细玩法。

2.1. 为什么需要流程变量

首先我们来看看为什么需要流程变量。

举一个简单的例子,假设我们有如下一个流程:

这是一个请假流程,那么谁请假、请几天、起始时间、请假理由等等,这些都需要说明,不然领导审批的依据是啥?那么如何传递这些数据,我们就需要流程变量。

2.2. 流程变量的分类

整体上来说,目前流程变量可以分为三种类型:

  1. 全局流程变量:在整个流程执行期间,这个流程变量都是有效的。
  2. 本地流程变量:这个只针对流程中某一个具体的 Task(任务)有效,这个任务执行完毕后,这个流程变量就失效了。
  3. 临时流程变量:顾名思义就是临时的,这个不会存入到数据库中。

在接下来的内容中,我会跟大家挨个介绍这些流程变量的用法。

2.3. 全局流程变量

假设我们就是上面这个请假流程,我们一起来看下流程变量的设置和获取。

2.3.1 启动时设置

第一种方式,就是我们可以在流程启动的时候,设置流程变量,如下:

@Test
void test01() {
    Map<String, Object> variables = new HashMap<>();
    variables.put("days", 10);
    variables.put("reason", "休息一下");
    variables.put("startTime", new Date());
    ProcessInstance pi = runtimeService.startProcessInstanceByKey("demo01", variables);
    logger.info("id:{},activityId:{}", pi.getId(), pi.getActivityId());
}
复制代码

我们可以在启动的时候为流程设置变量,小伙伴们注意到,流程变量的 value 也可以是一个对象(不过这个对象要能够序列化,即实现了 Serializable 接口),然后在启动的时候传入这个变量即可。

我们在流程启动日志中搜索 休息一下 四个字,可以找到和流程变量相关的 SQL,一共有两条,如下:

insert into ACT_HI_VARINST (ID_, PROC_INST_ID_, EXECUTION_ID_, TASK_ID_, NAME_, REV_, VAR_TYPE_, SCOPE_ID_, SUB_SCOPE_ID_, SCOPE_TYPE_, BYTEARRAY_ID_, DOUBLE_, LONG_ , TEXT_, TEXT2_, CREATE_TIME_, LAST_UPDATED_TIME_) values ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) , ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) , ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )

INSERT INTO ACT_RU_VARIABLE (ID_, REV_, TYPE_, NAME_, PROC_INST_ID_, EXECUTION_ID_, TASK_ID_, SCOPE_ID_, SUB_SCOPE_ID_, SCOPE_TYPE_, BYTEARRAY_ID_, DOUBLE_, LONG_ , TEXT_, TEXT2_) VALUES ( ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) , ( ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) , ( ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
复制代码

从标名称上大概就能看出来,ACT_HI_VARINST 是存储流程执行的历史信息的,ACT_RU_VARIABLE 则是保存流程运行时候的信息的。

我们打开 ACT_RU_VARIABLE 表来看一下:

从表中我们可以看到,每一个流程变量都有对应的流程实例 ID,这就说明这些流程变量是属于某一个流程实例的,所以我们可以按照如下方式来查询流程变量:

@Test
void test01() {
    List<Execution> list = runtimeService.createExecutionQuery().list();
    for (Execution execution : list) {
        Object reason = runtimeService.getVariable(execution.getId(), "reason");
        logger.info("reason:{}", reason);
    }
}
复制代码

对应的查询 SQL 如下:

: ==>  Preparing: select * from ACT_RU_VARIABLE WHERE EXECUTION_ID_ = ? AND TASK_ID_ is null AND NAME_ = ?
: ==> Parameters: 6fdd2007-4c3a-11ed-aa7e-acde48001122(String), reason(String)
: <==      Total: 1
复制代码

可以看到,这个就是去 ACT_RU_VARIABLE 表中进行查询,查询条件中包含了变量的名称。

当然,我们也可以直接查询某一个流程的所有变量,如下:

@Test
void test02() {
    List<Execution> list = runtimeService.createExecutionQuery().list();
    for (Execution execution : list) {
        Map<String,Object> variables = runtimeService.getVariables(execution.getId());
        logger.info("variables:{}", variables);
    }
}
复制代码

这个对应的查询 SQL 如下:

 : ==>  Preparing: select * from ACT_RU_VARIABLE WHERE EXECUTION_ID_ = ? AND TASK_ID_ is null
 : ==> Parameters: 6fdd2007-4c3a-11ed-aa7e-acde48001122(String)
 : <==      Total: 3
复制代码

可以看到,这个跟上面的那个差不多,只不过少了 NAME_ 这个条件。

2.3.2 通过 Task 设置

我们也可以在流程启动成功之后,再去设置流程变量,步骤如下:

首先启动一个流程:

@Test
void test01() {
    ProcessInstance pi = runtimeService.startProcessInstanceByKey("demo01");
    logger.info("id:{},activityId:{}", pi.getId(), pi.getActivityId());
}
复制代码

然后设置流程变量:

@Test
void test03() {
    Task task = taskService.createTaskQuery().singleResult();
    taskService.setVariable(task.getId(), "days", 10);
    Map<String, Object> variables = new HashMap<>();
    variables.put("reason", "休息一下");
    variables.put("startTime", new Date());
    taskService.setVariables(task.getId(),variables);
}
复制代码

查询到某一个 Task,然后设置流程变量,上面这段代码和小伙伴们演示了两种设置方式:

  • 逐个设置
  • 直接设置一个 Map

上面这个设置流程变量的方式,本质上还是往 ACT_HI_VARINST 和 ACT_RU_VARIABLE 表中插入数据。具体的 SQL 也和前面的一样,我就不贴出来了。

2.3.3 完成任务时设置

也可以在完成一个任务的时候设置流程变量,如下:

@Test
void test04() {
    Task task = taskService.createTaskQuery().singleResult();
    Map<String, Object> variables = new HashMap<>();
    variables.put("reason", "休息一下");
    variables.put("startTime", new Date());
    variables.put("days", 10);
    taskService.complete(task.getId(),variables);
}
复制代码

底层涉及到的 SQL 都跟前面一样,我就不赘述了。

2.3.4 通过流程设置

由于是全局流程变量,所以我们也可以通过 RuntimeService 来进行设置,如下:

@Test
void test05() {
    Execution execution = runtimeService.createExecutionQuery().singleResult();
    runtimeService.setVariable(execution.getId(), "days", 10);
    Map<String, Object> variables = new HashMap<>();
    variables.put("reason", "休息一下");
    variables.put("startTime", new Date());
    runtimeService.setVariables(execution.getId(), variables);
}
复制代码

好啦,一共就是这四种方式。

2.4. 本地流程变量

第三小节我们说的全局流程变量是和某一个具体的流程绑定的,而本地流程变量则不同,本地流程变量和某一个 Task 绑定。

2.4.1 通过 Task 设置

假设我们启动流程之后,通过 Task 来设置一个本地流程变量,方式如下:

@Test
void test03() {
    Task task = taskService.createTaskQuery().singleResult();
    taskService.setVariableLocal(task.getId(), "days", 10);
    Map<String, Object> variables = new HashMap<>();
    variables.put("reason", "休息一下");
    variables.put("startTime", new Date());
    taskService.setVariables(task.getId(),variables);
}
复制代码

上面这段代码中,我设置了一个本地变量,两个全局变量,设置完成后,我们去 ACT_RU_VARIABLE 表中来查看一下具体的效果。

大家看到,由于 days 是本地变量,所以它的 TASK_ID_ 有值,这个好理解,说明 days 这个变量和这个具体的 Task 是有关的。

此时如果我们完成这个 Task,代码如下:

@Test
void test06() {
    Task task = taskService.createTaskQuery().singleResult();
    taskService.complete(task.getId());
}
复制代码

完成之后,再来查看 ACT_RU_VARIABLE 表,如下:

我们发现本地变量 days 已经没有了。因为上一个 Task 都已经执行完毕了,这个时候如果还是按照第三小节介绍的方式去查询变量,就查不到 days 了。此时如果需要查询到曾经的 days 变量,得去历史表中查询了,方式如下:

@Test
void test07() {
    ProcessInstance pi = runtimeService.createProcessInstanceQuery().singleResult();
    List<HistoricVariableInstance> list = historyService.createHistoricVariableInstanceQuery().processInstanceId(pi.getId()).list();
    for (HistoricVariableInstance hvi : list) {
        logger.info("name:{},type:{},value:{}", hvi.getVariableName(), hvi.getVariableTypeName(), hvi.getValue());
    }
}
复制代码

这是流程本地变量的特点,当然相关的方法还有好几个,这里列出来给小伙伴们参考:

  • org.flowable.engine.TaskService#complete(java.lang.String, java.util.Map<java.lang.String,java.lang.Object>, boolean):在完成一个 Task 的时候,如果传递了变量,则可以通过第三个参数来控制这个变量是全局的还是本地的,true 表示这个变量是本地的。
  • org.flowable.engine.RuntimeService#setVariableLocal:为某一个执行实例设置本地变量。
  • org.flowable.engine.RuntimeService#setVariablesLocal:同上,批量设置。

好啦,这就是本地流程变量。

2.5. 临时流程变量

临时流程变量是不存数据库的,一般来说我们可以在启动流程或者完成任务的时候使用,用法如下:

@Test
void test21() {
    Map<String, Object> variables = new HashMap<>();
    variables.put("reason", "休息一下");
    variables.put("startTime", new Date());
    ProcessInstance pi = runtimeService
            .createProcessInstanceBuilder()
            .transientVariable("days", 10)
            .transientVariables(variables)
            .processDefinitionKey("demo01")
            .start();
    logger.info("id:{},activityId:{}", pi.getId(), pi.getActivityId());
}
复制代码

上面这段代码涉及到的流程变量就是临时流程变量,它是不会存入到数据库中的。

也可以在完成一个任务的时候设置临时变量,如下:

@Test
void test22() {
    Task task = taskService.createTaskQuery().singleResult();
    Map<String, Object> transientVariables = new HashMap<>();
    transientVariables.put("days", 10);
    taskService.complete(task.getId(), null, transientVariables);
}
复制代码

这个临时变量也是不会存入到数据库中的。

好啦,关于流程变量,今天就和小伙伴们先说这么多~

3. 历史流程

[TOC]

在之前的文章中松哥和小伙伴们聊过,正在执行的流程信息是保存在以 ACT_RU_ 为前缀的表中,执行完毕的流程信息则保存在以 ACT_HI_ 为前缀的表中,也就是流程历史信息表,当然这个历史信息表继续细分的话,还有好多种,今天我们就来聊一聊这个话题。

假设我有如下一个流程:

当这个流程执行完毕后,以 ACT_RU_ 为前缀的表中的数据均已清空,现在如果想查看刚刚执行过的流程信息,我们就得去以 ACT_HI_ 为前缀的表中。

3.1. 历史流程信息

历史流程信息查看,方式如下:

@Test
void test05() {
    List<HistoricProcessInstance> list = historyService.createHistoricProcessInstanceQuery().finished().list();
    for (HistoricProcessInstance hpi : list) {
        logger.info("name:{},startTime:{},endTime:{}",hpi.getName(),hpi.getStartTime(),hpi.getEndTime());
    }
}
复制代码

调用的时候执行的 finished() 方法表示查询已经执行完毕的流程信息(从这里也可以看出,对于未执行完毕的流程信息也会保存在历史表中)。

我们来看下这个查询对应的 SQL,如下:

SELECT RES.* , DEF.KEY_ as PROC_DEF_KEY_, DEF.NAME_ as PROC_DEF_NAME_, DEF.VERSION_ as PROC_DEF_VERSION_, DEF.DEPLOYMENT_ID_ as DEPLOYMENT_ID_ from ACT_HI_PROCINST RES left outer join ACT_RE_PROCDEF DEF on RES.PROC_DEF_ID_ = DEF.ID_ WHERE RES.END_TIME_ is not NULL order by RES.ID_ asc
复制代码

从这个 SQL 中可以看到,这个查询本质上就是查询的 ACT_HI_PROCINST 表。如下图:

如果我们在查询的时候不限制流程是否执行完毕,那么我们的查询方法如下:

@Test
void test05() {
    List<HistoricProcessInstance> list = historyService.createHistoricProcessInstanceQuery().list();
    for (HistoricProcessInstance hpi : list) {
        logger.info("name:{},startTime:{},endTime:{}",hpi.getName(),hpi.getStartTime(),hpi.getEndTime());
    }
}
复制代码

对应的查询 SQL 如下:

SELECT RES.* , DEF.KEY_ as PROC_DEF_KEY_, DEF.NAME_ as PROC_DEF_NAME_, DEF.VERSION_ as PROC_DEF_VERSION_, DEF.DEPLOYMENT_ID_ as DEPLOYMENT_ID_ from ACT_HI_PROCINST RES left outer join ACT_RE_PROCDEF DEF on RES.PROC_DEF_ID_ = DEF.ID_ order by RES.ID_ asc
复制代码

和前面的 SQL 相比,后面的 SQL 少了 WHERE RES.END_TIME_ is not NULL 条件,也就是说,判断一个流程是否执行完毕,就看它的 END_TIME_ 是否为空,不为空就表示流程已经执行结束了,为空就表示流程尚在执行中。

3.2. 历史任务查询

刚刚我们查询的是历史流程,接下来我们来看下历史任务,也就是查询一个流程中执行过的 Task 信息,如下表示查询所有的历史流程任务:

@Test
void test06() {
    List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery().list();
    for (HistoricTaskInstance hti : list) {
        logger.info("name:{},assignee:{},createTime:{},endTime:{}",hti.getName(),hti.getAssignee(),hti.getCreateTime(),hti.getEndTime());
    }
}
复制代码

这个查询对应的 SQL 如下:

SELECT RES.* from ACT_HI_TASKINST RES order by RES.ID_ asc
复制代码

可以看到,历史任务表就是 ACT_HI_TASKINST,如下图:

当然,这里还有很多其他的玩法,例如查询某一个流程已经执行完毕的历史任务,如下:

@Test
void test07() {
    List<HistoricProcessInstance> instanceList = historyService.createHistoricProcessInstanceQuery().list();
    for (HistoricProcessInstance hpi : instanceList) {
        List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery().processInstanceId(hpi.getId()).finished().list();
        for (HistoricTaskInstance hti : list) {
            logger.info("name:{},assignee:{},createTime:{},endTime:{}", hti.getName(), hti.getAssignee(), hti.getCreateTime(), hti.getEndTime());
        }
    }
}
复制代码

这个里边的查询历史任务的 SQL 如下:

SELECT RES.* from ACT_HI_TASKINST RES WHERE RES.PROC_INST_ID_ = ? and RES.END_TIME_ is not null order by RES.ID_ asc
复制代码

可以看到,跟前面相比,多了两个条件:

  1. 流程实例 ID
  2. 流程结束时间不为 null

从这里也可以看出来,这个 finish 方法的执行逻辑跟我们前面讲的是一样的。

3.3. 历史活动查询

历史任务就是各种 Task,历史活动则包括跟多内容,像开始/结束节点,连线等等这些信息都算是活动,这个在之前的文章中松哥已经和大家介绍过了。

查询代码如下:

@Test
void test08() {
    List<HistoricActivityInstance> list = historyService.createHistoricActivityInstanceQuery().list();
    for (HistoricActivityInstance hai : list) {
        logger.info("name:{},startTime:{},assignee:{},type:{}",hai.getActivityName(),hai.getStartTime(),hai.getAssignee(),hai.getActivityType());
    }
}
复制代码

这个查询对应的 SQL 如下:

SELECT RES.* from ACT_HI_ACTINST RES order by RES.ID_ asc
复制代码

可以看到,ACT_HI_ACTINST 表中保存了历史活动信息。

3.4. 历史变量查询

查询流程执行的历史变量,方式如下:

@Test
void test09() {
    HistoricProcessInstance pi = historyService.createHistoricProcessInstanceQuery().singleResult();
    List<HistoricVariableInstance> list = historyService.createHistoricVariableInstanceQuery().processInstanceId(pi.getId()).list();
    for (HistoricVariableInstance hvi : list) {
        logger.info("name:{},type:{},value:{}", hvi.getVariableName(), hvi.getVariableTypeName(), hvi.getValue());
    }
}
复制代码

这个查询对应的 SQL 如下:

SELECT RES.* from ACT_HI_VARINST RES WHERE RES.PROC_INST_ID_ = ? order by RES.ID_ asc
复制代码

可以看到流程的历史变量信息保存在 ACT_HI_VARINST 表中。

3.5. 历史日志查询

有的小伙伴看到日志这两个字可能会觉得奇怪,咦?流程执行还有日志吗?没听说过呀!

其实历史日志查询就是前面那几种的一个集大成者,用法如下:

@Test
void test10() {
    HistoricProcessInstance pi = historyService.createHistoricProcessInstanceQuery().singleResult();
    ProcessInstanceHistoryLog historyLog = historyService.createProcessInstanceHistoryLogQuery(pi.getId())
            //包括历史活动
            .includeActivities()
            //包括历史任务
            .includeTasks()
            //包括历史变量
            .includeVariables()
            .singleResult();
    logger.info("id:{},startTime:{},endTime:{}", historyLog.getId(), historyLog.getStartTime(), historyLog.getEndTime());
    List<HistoricData> historicData = historyLog.getHistoricData();
    for (HistoricData data : historicData) {
        if (data instanceof HistoricActivityInstance) {
            HistoricActivityInstance hai = (HistoricActivityInstance) data;
            logger.info("name:{},type:{}", hai.getActivityName(), hai.getActivityType());
        }
        if (data instanceof HistoricTaskInstance) {
            HistoricTaskInstance hti = (HistoricTaskInstance) data;
            logger.info("name:{},assignee:{}", hti.getName(), hti.getAssignee());
        }
        if (data instanceof HistoricVariableInstance) {
            HistoricVariableInstance hvi = (HistoricVariableInstance) data;
            logger.info("name:{},type:{},value:{}", hvi.getVariableName(), hvi.getVariableTypeName(), hvi.getValue());
        }
    }
}
复制代码

这个里边,首先是查询基本的流程日志信息,这个本质上就是查询历史流程实例信息,对应的 SQL 如下:

select RES.*, DEF.KEY_ as PROC_DEF_KEY_, DEF.NAME_ as PROC_DEF_NAME_, DEF.VERSION_ as PROC_DEF_VERSION_, DEF.DEPLOYMENT_ID_ as DEPLOYMENT_ID_ from ACT_HI_PROCINST RES left outer join ACT_RE_PROCDEF DEF on RES.PROC_DEF_ID_ = DEF.ID_ where PROC_INST_ID_ = ?
复制代码

接下来我写了三个 include,每一个 include 都对应一句 SQL:

includeActivities 对应的 SQL 如下:

SELECT RES.* from ACT_HI_ACTINST RES WHERE RES.PROC_INST_ID_ = ? order by RES.ID_ asc
复制代码

includeTasks 对应的 SQL 如下:

SELECT RES.* from ACT_HI_TASKINST RES WHERE RES.PROC_INST_ID_ = ? order by RES.ID_ asc
复制代码

includeVariables 对应的 SQL 如下:

SELECT RES.* from ACT_HI_VARINST RES WHERE RES.PROC_INST_ID_ = ? order by RES.ID_ asc
复制代码

最终查询完成后,调用 getHistoricData 方法可以查看这些额外的数据,List 集合中存放的 HistoricData 也分为不同的类型:

  • includeActivities 方法对应最终查询出来的类型是 HistoricActivityInstance。
  • includeTasks 方法对应最终查询出来的类型是 HistoricTaskInstance。
  • includeVariables 方法对应最终查询出来的类型是 HistoricVariableInstance。

在遍历的时候通过类型判断去查看具体是哪一种变量类型。

综上,这个历史日志查询其实就是一个集大成者。

3.6. 历史权限查询

这个是用来查询流程或者任务的处理人,例如查询流程的处理人,方式如下:

@Test
void test11() {
    HistoricProcessInstance pi = historyService.createHistoricProcessInstanceQuery().singleResult();
    List<HistoricIdentityLink> links = historyService.getHistoricIdentityLinksForProcessInstance(pi.getId());
    for (HistoricIdentityLink link : links) {
        logger.info("userId:{}",link.getUserId());
    }
}
复制代码

这个是查询流程对应的处理人,对应的 SQL 如下:

select * from ACT_HI_IDENTITYLINK where PROC_INST_ID_ = ?
复制代码

如果想查询任务的处理人,对应的方式如下:

@Test
void test12() {
    String taskName = "提交请假申请";
    HistoricTaskInstance hti = historyService.createHistoricTaskInstanceQuery().taskName(taskName).singleResult();
    List<HistoricIdentityLink> links = historyService.getHistoricIdentityLinksForTask(hti.getId());
    for (HistoricIdentityLink link : links) {
        logger.info("{} 任务的处理人是 {}",taskName,link.getUserId());
    }
}
复制代码

这个查询对应的 SQL 如下:

select * from ACT_HI_IDENTITYLINK where TASK_ID_ = ?
复制代码

和前面的相比,其实就多了一个查询条件 TASK_ID_

3.7. 自定义查询 SQL

和前面讲的很多查询类似,当我们弄懂了每一个历史查询的 API 操作的是哪一个数据表,就会发现,历史数据的查询,也可以自定义 SQL。

举个例子和小伙伴们看下,例如查询某一个流程已经执行完毕的历史任务:

@Test
void test13() {
    List<HistoricProcessInstance> instanceList = historyService.createHistoricProcessInstanceQuery().list();
    for (HistoricProcessInstance hpi : instanceList) {
        List<HistoricTaskInstance> list = historyService.createNativeHistoricTaskInstanceQuery()
                .sql("SELECT RES.* from ACT_HI_TASKINST RES WHERE RES.PROC_INST_ID_ = #{pid} and RES.END_TIME_ is not null order by RES.ID_ asc")
                .parameter("pid",hpi.getId()).list();
        for (HistoricTaskInstance hti : list) {
            logger.info("name:{},assignee:{},createTime:{},endTime:{}", hti.getName(), hti.getAssignee(), hti.getCreateTime(), hti.getEndTime());
        }
    }
}
复制代码

flowable 底层是 MyBatis,所有 SQL 中参数的传递形式和 MyBatis 一致。

3.8. 历史数据记录级别

Flowable 需要记录哪些历史数据,有一个日志级别用来描述这个事情,默认有四种级别:

  • None: 这个表示不存储任何历史信息,好处是流程执行的时候效率会比较快,坏处是流程执行结束后,看不到曾经执行过的流程信息了。
  • Activity: 这个会存储所有流程实例和活动实例,在流程实例结束时,顶级流程实例变量的最新值将复制到历史变量实例中,不会存储详细信息。
  • Audit: 在 Activity 的基础上,还会存储历史详细信息,包括权限信息等。默认的日志记录级别即次。
  • Full: 这个是在 Audit 的基础上,还会存储变量的变化信息,这会记录大量的数据,也会导致流程执行变慢。

一共就这四种级别,在 Spring Boot 项目中,如果我们想要配置这个日志记录的级别,其实非常方便,直接在 application.properties 中进行配置即可,如下:

flowable.history-level=none
复制代码

配置加了这个配置,我们随便启动一个流程,然后去查询 ACT_HI_ 系列的表,发现都是空的,没有数据。

如果我们将历史日志记录的级别改为 activity,那么就会记录下来流程信息以及活动信息,但是像执行的 Task 这些信息都是没有的(ACT_HI_TASKINST),包括流程参与者的信息(ACT_HI_IDENTITYLINK)等都不会记录下来。

如果我们将历史日志记录的级别改为 audit,则上面提到的这几种日志就都会记录下来。但是 ACT_HI_DETAIL 表还是空的,详细一个流程变量的变化过程不会被记录下来。

如果我们将日志记录级别改为 full,那么将会记录下更多的信息。ACT_HI_DETAIL 表中会记录下流程变量的详细信息。

整个过程我就不给小伙伴们演示了大家可以自行尝试。

好啦,关于历史数据的查询,松哥先和小伙伴们聊这么多~下篇文章我们继续~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/42821.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

苦等官方阿里云盘TV版久矣,终于期待一款TV版。

苦等官方阿里云盘TV版久矣&#xff0c;终于期待一款TV版。 一、获取链接&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/19Z-2Lb4-lOSU14M83rivVA 提取码&#xff1a;o2z0 二、产品&#xff1a; 1、文件列表&#xff1a;确定键打开文件或文件夹&#xff0c;菜单键进…

华为19级专家10年心血终成百页负载均衡高并发网关设计实战文档

负载均衡&#xff08;LoadBalance&#xff09;的字面意思是将工作负载分担到多个工作单元上进行执行&#xff0c;它建立在现有网络结构之上&#xff0c;是构建分布式服务、大型网络应用的关键组件。 近十几年来&#xff0c;负载均衡技术层出不穷&#xff0c;令人眼花缭乱。如果…

[思考进阶]01 如何克服自己的无知?

除了要提升自己的技术能力&#xff0c;思维的学习和成长也非常非常重要&#xff0c;特推出此[思考进阶]系列&#xff0c;进行刻意练习&#xff0c;从而提升自己的认知。 有段时间我特别喜欢研究一些定律和法则&#xff0c;比如&#xff1a;熵增定律、懒蚂蚁效应、蝴蝶效应、吸引…

Java搭建宝塔部署实战毕设项目物品分类springboot管理系统源码

大家好啊&#xff0c;我是测评君&#xff0c;欢迎来到web测评。 本期给大家带来一套Java开发的物品分类springboot管理系统源码&#xff0c;适合准备做毕业设计的同学&#xff0c;可以下载搭建起来&#xff0c;在这基础上加一些功能&#xff0c;改改UI&#xff0c;随便写篇答题…

解决办法‘npm‘ 不是内部或外部命令,也不是可运行的程序或批处理文件。

今天使用了nvm这个node的保姆级版本管理工具,的确很香。 首先打开官网nvm.uihtm.com/ 下载安装过程很顺利&#xff0c;但是验证node及npm是否安装成功时&#xff0c;出现下面错误提示。 C:\Users\Administrator>npm -v npm 不是内部或外部命令&#xff0c;也不是可运行的…

里奥哈大使撰文 | 来一场云旅行吧,盘点里奥哈那些美轮美奂的酒庄~

俗话说得好&#xff0c;“里奥哈美酒夜光杯&#xff0c;欲买机票马上飞”&#xff08;说得太好了&#xff0c;反正也去不了下次别说了&#xff09;既然疫情当下&#xff0c;无法来一场说走就走的旅行&#xff0c;那我们就来云游一下里奥哈这个美轮美奂的酒庄吧&#xff01; 说起…

2023年浙大MEM英语二作文干货模版:临阵磨枪可用

这些年来&#xff0c;通过杭州达立易考教育对英语二小作文的总结发现&#xff0c;书信形式成为主要考察的内容&#xff0c;而随着形式的变化和难度的提升&#xff0c;复合型题材逐渐成为侧重点&#xff0c;比如道歉信加建议信、感谢信加建议信等。我们不建议原班照背模版&#…

汇编语言快速回顾(以x86_64为例)

GAS (GNU Assembler) 是一款基于 Linux 的汇编器&#xff0c;主要供 GNU 项目使用&#xff0c;用于对 Linux 内核及其他软件进行进行汇编。 MASM (Microsoft Macro Assembler) 是微软操作系统的专属汇编器&#xff0c;于 1981 年随 Visual Studio 一起发布。 大学本科教育中有两…

小学生python游戏编程arcade----精灵调用图片的两种类

小学生python游戏编程arcade----精灵调用图片的两种类前言精灵调用图片的两种类1、第一种类的定义1.1 以文件名及缩放比例做初始参数1.2 利用变换图片的颜色更换角色的使用1.3 代码分析转换文件来不及调用&#xff0c;时间问题&#xff1f;&#xff1f;&#xff1f;&#xff1f…

【数据结构初阶】链式二叉树接口实现+痛苦的OJ题

递归心决&#xff1a;(xdm好好感悟) 1.确定递归的结束条件 2.确定递归的单层逻辑 3.确定递归的参数和返回值 文章目录一、链式二叉树接口1.二叉树的结构体2.手动造一棵二叉树3.二叉树前、中、后序遍历(递归的神圣大门开启)4.二叉树的结点个数5.二叉树的叶子结点个数6.二叉树的…

【毕业设计】深度学习动物识别系统 - 图像识别 python OpenCV

文章目录0 前言1 课题背景2 实现效果3 卷积神经网络3.1卷积层3.2 池化层3.3 激活函数&#xff1a;3.4 全连接层3.5 使用tensorflow中keras模块实现卷积神经网络4 inception_v3网络5 最后0 前言 &#x1f525; Hi&#xff0c;大家好&#xff0c;这里是丹成学长的毕设系列文章&a…

Web前端大作业、基于HTML+CSS+JavaScript响应式个人相册博客网站

&#x1f389;精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

【DL with Pytorch】第 1 章 :深度学习与PyTorch简介

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

1.每天进步一点点------爬虫应用场景

14天学习训练营导师课程&#xff1a; 杨鑫《Python 自学编程基础》 杨鑫《 Python 网络爬虫基础》 杨鑫《 Scrapy 爬虫框架实战和项目管理》 杨老师推荐自学Python的网站 w3schools 传送门 geeksforgeeks 传送门 realpython 传送门 学习之前读一读 来看看爬虫合不合法 传送…

安洵杯 2022 复现

babyphp 知识点&#xff1a;原生类的利用&#xff0c;session 反序列化<?php //something in flag.phpclass A {public $a;public $b;public function __wakeup(){$this->a "babyhacker";}public function __invoke(){if (isset($this->a) && $t…

[附源码]SSM计算机毕业设计文章管理系统查重PPTJAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Java 多线程为啥要有ThreadLocal,怎么用,这篇讲全了!

前面我们学习的线程并发时的同步控制&#xff0c;是为了保证多个线程对共享数据争用时的正确性的。那如果一个操作本身不涉及对共享数据的使用&#xff0c;相反&#xff0c;只是希望变量只能由创建它的线程使用&#xff08;即线程隔离&#xff09;就需要到线程本地存储了。 Ja…

艾美捷曲妥珠单抗Trastuzumab化学性质和特异性说明

艾美捷曲妥珠单抗Trastuzumab是人源化IgG1κ单克隆抗体&#xff0c;其以高亲和力选择性结合人表皮生长因子受体2蛋白HER2的细胞外结构域。曲妥珠单抗通过重组DNA技术在哺乳动物细胞&#xff08;中国仓鼠卵巢&#xff09;中产生。 艾美捷曲妥珠单抗Trastuzumab化学性质&#xff…

[附源码]SSM计算机毕业设计鲜花销售管理系统JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Set 集合概述与使用

目录 一、概述 1.特点&#xff1a; 2.方法&#xff1a; 二、Set接口的使用 三、Set实现类 1.HashSet *** &#xff08;1&#xff09;简单应用&#xff1a; &#xff08;2&#xff09;存储方式 2.TreeSet &#xff08;1&#xff09;红黑树 &#xff08;2&#xff09…