解决Activiti异常java.sql.SQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails


如果在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的同学只能自己修复了。


发表回复

您的电子邮箱地址不会被公开。

正在检测……