如果在DelegateTask和其所属的Execution中设置同名的本地变量,Activiti在删除Execution时可能会抛出异常java.sql.SQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (`XXX_SCHEMA_NAME`.`act_ru_variable
`, CONSTRAINT `ACT_FK_VAR_EXE
` FOREIGN KEY (`EXECUTION_ID_
`) REFERENCES `act_ru_execution
` (`ID_
`))。
这个问题之前在《Activiti 6 踩坑日记》中记录了一下,但没有时间详细研究,今天花时间好好研究了一下,终于把这个问题解决了。
Activiti删除Execution调用的是ExecutionEntityManagerImpl#deleteExecutionAndRelatedData这个方法,在该方法中会调用ExecutionEntityManagerImpl#deleteDataForExecution删除Execution关联的其他数据,包括Execution的本地变量。在删除Execution的本地变量时,该方法首先加载Execution的所有本地变量,然后逐个删除。Debug时发现加载的Execution本地变量有问题,有些变量被Task的同名本地变量覆盖了,导致Execution的部分本地变量并没有被删除,最终导致Activiti抛出上面的异常。
Execution加载本地变量调用的是VariableScopeImpl#ensureVariableInstancesInitialized这个方法,该方法会调用ExecutionEntityImpl#loadVariableInstances方法查询数据库,该方法最终会调用AbstractDataManager#getList这个方法,该方法会先查询数据库,然后再从本地实体缓存中查找,最后合并这两部分结果并返回。
在查找本地缓存时,Activiti先获取本地缓存中的所有变量实体,然后调用VariableByExecutionIdMatcher#isRetained方法判断该变量是否是Execution的变量,如果是则保留。问题就出在VariableByExecutionIdMatcher#isRetained方法中,该方法的代码如下:
public class VariableByExecutionIdMatcher extends CachedEntityMatcherAdapter<VariableInstanceEntity> {
@Override
public boolean isRetained(VariableInstanceEntity variableInstanceEntity, Object parameter) {
return variableInstanceEntity.getExecutionId() != null
&& variableInstanceEntity.getExecutionId().equals((String) parameter);
}
}
从上面的代码可以看出,在判断变量是否是Execution的本地变量时,只检查了变量的executionId字段,而没有检查变量的taskId字段是否为null。如果taskId字段不为null,则该变量应该属于对应的Task,并不属于Execution。
上面这个问题会导致ExecutionEntityImpl#loadVariableInstances这个方法返回的变量,不仅包括Execution的本地变量,还会包含缓存中Execution关联的所有Task的本地变量。现在我们回头看一下最开始的VariableScopeImpl#ensureVariableInstancesInitialized这个方法的代码。
protected void ensureVariableInstancesInitialized() {
if (variableInstances == null) {
variableInstances = new HashMap<String, VariableInstanceEntity>();
CommandContext commandContext = Context.getCommandContext();
if (commandContext == null) {
throw new ActivitiException("lazy loading outside command context");
}
Collection<VariableInstanceEntity> variableInstancesList = loadVariableInstances();
for (VariableInstanceEntity variableInstance : variableInstancesList) {
variableInstances.put(variableInstance.getName(), variableInstance);
}
}
}
可以看到ExecutionEntityImpl#loadVariableInstances返回的变量列表,最后又按照变量名保存到variableInstances这个HashMap中,因此同名的变量会相互覆盖。由于ExecutionEntityImpl#loadVariableInstances返回的变量没有固定的顺序,因此Task变量可能会覆盖Execution变量,也可能不会覆盖Execution变量。如果覆盖了Execution变量,在删除Execution时就会报错,如果没有覆盖,在删除Execution是则不会报错。这也解释了,测试时有时候会报错,有时候又不会报错的现象。
Activiti的这个BUG不仅会导致删除Execution时报错,还会导致读取Execution本地变量时读取到错误的值,影响业务流程,应该算是一个比较严重的BUG。
这个BUG我已经提交给官方了,The local variable loaded by Execution is incorrect ,使用Activit 7的同学可以等官方修复。使用Activiti 6的同学只能自己修复了。