配置导入事务问题与修复总结

配置导入事务问题与修复总结

本文档总结配置导入(Excel 模板导入)过程中出现的 UnexpectedRollbackException 与前端错误信息不对的问题、根因及修复方案。


一、问题现象

  1. 后端日志org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
  2. 前端展示:页面提示「配置导入失败:Transaction rolled back because it has been marked as rollback-only」,而不是真实的业务错误(如「数据库表不存在:xxx」)
  3. 调试现象 :Sheet1 的 for 循环里已用 try-catch 把异常包进 result,并在 78-79 行正常 return result,但 Controller 仍进入异常捕获分支(110-111 行)

二、根因分析

2.1 事务与"异常即标记回滚"

  • ConfigImportServiceImpl.importFromExcel(InputStream) 使用 @Transactional(rollbackFor = Exception.class),整段导入在一个事务中执行。
  • Spring 行为 :只要在事务内任意时刻 抛出了符合 rollbackFor 的异常,当前事务就会被标记为 rollback-only
    标记发生在异常抛出的瞬间,与后续是否在业务代码里 catch 无关。

2.2 Sheet1 场景下的实际流程

  1. Sheet1 的 for 循环中调用 factTableSchemaService.importTableFromDatabase(...)
  2. 若某张表不存在,FactTableSchemaServiceImpl 抛出 IllegalArgumentException("数据库表不存在:" + tableName)
  3. 此时 :当前事务(即 importFromExcel 所在事务)被标记为 rollback-only。
  4. 异常随后在 for 循环的 catch 中被捕获,仅执行 result.addError(...),未 rethrow。
  5. 代码继续执行,到 78-79 行 if (!result.isSuccess() && !result.getErrors().isEmpty()) { return result; }正常 return result
  6. 方法正常返回后,Spring 事务拦截器执行 commit
  7. 发现事务已是 rollback-only,不允许提交,于是抛出 UnexpectedRollbackException
  8. 该异常冒泡到 Controller,被 110-111 行 catch (Exception e) 捕获,前端看到的是 commit 失败的消息,而非 result 中已写好的「表[xxx]导入失败: 数据库表不存在:xxx」。

2.3 两种失败路径对比

场景 执行路径 结果
仅 Sheet1 出错 for 内 catch → 78-79 return result → commit 时抛 UnexpectedRollbackException → Controller 进 catch 前端看到 rollback 提示,result 中正确错误信息未展示
Sheet2/3/5 或字段配置出错 后续某步抛异常 → 外层 catch 后 rethrow → Controller 进 catch 前端看到真实异常信息(修复 rethrow 后)

三、修复方案

3.1 修复一:Sheet1 表导入使用独立事务(REQUIRES_NEW)

目的:让 Sheet1 中单表导入失败时,只回滚该次调用的事务,不污染外层导入事务。

修改FactTableSchemaServiceImpl.importTableFromDatabase

  • 原:@Transactional(rollbackFor = Exception.class)
  • 现:@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)

效果

  • 每次调用 importTableFromDatabase 都会挂起外层事务并开启一个新事务。
  • 某张表导入失败抛异常时,仅该新事务被标记 rollback-only 并回滚。
  • 异常在 Sheet1 的 for 中被 catch 并写入 result外层 importFromExcel 事务未被标记
  • 78-79 行 return result 后,外层事务正常 commit,Controller 收到带错误信息的 result,走 success(result),前端可正确展示 result 中的错误列表。

3.2 修复二:外层 catch 不再吞异常(Rethrow)

目的 :当异常来自 Sheet2/3/5 或字段配置等未内部 catch 的步骤时,将真实异常抛出,避免只看到 UnexpectedRollbackException。

修改ConfigImportServiceImpl.importFromExcel(InputStream) 最外层 catch

  • 原:catch (Exception e) { ... result.addError(...); result.setSuccess(false); } 后直接 return result
  • 现:在 catch 末尾增加 throw new RuntimeException(e.getMessage() != null ? e.getMessage() : "配置导入失败", e);

效果

  • 后续 Sheet 或字段配置中一旦抛异常,会直接抛出,事务按预期回滚。
  • Controller 的 catch 收到的是包装后的 RuntimeException,getMessage() 为原始业务错误信息,前端可显示「配置导入失败:数据库表不存在:xxx」等真实原因。

四、涉及文件与位置

文件 修改内容
backend/.../FactTableSchemaServiceImpl.java importTableFromDatabase 增加 propagation = Propagation.REQUIRES_NEW,并增加 Propagation 的 import
backend/.../ConfigImportServiceImpl.java 最外层 catch 中在写入 result 后 throw new RuntimeException(..., e)

五、小结

  • 问题本质:事务内抛出的异常会使事务被标记 rollback-only;若异常被业务 catch 且未 rethrow,方法仍会"正常返回",但 commit 时因 rollback-only 再抛 UnexpectedRollbackException,前端只能看到后者。
  • Sheet1 场景 :通过 importTableFromDatabase 使用 REQUIRES_NEW,把单表导入放到独立事务中,表导入失败只回滚该次调用,外层可正常 return result 并 commit,前端拿到 result 中的错误信息。
  • 后续 Sheet 场景:通过外层 catch 后 rethrow,保证真实异常传到 Controller,前端显示真实错误信息。

相关功能说明见:docs/导入模板/配置导入模板说明.md

相关推荐
王夏奇4 小时前
Python-对excel文件操作的总览
开发语言·python·excel
林月明4 小时前
【Coze基础】Excel保存CSV文件时其设置为UTF-8编码,将数据导入数据库中
数据库·sql·oracle·excel·code·学习经验
howard20054 小时前
Pandas读取包含多个工作表的Excel文件
excel·pandas
Dylan~~~4 小时前
Excel MCP Server:用自然语言操控 Excel,开启“对话式电子表格“新时代
excel·ai编程
catoop5 小时前
Excel 实战技巧:利用 OFFSET 统计 “标识行” 下方的数值总和
excel
SHolmes18541 天前
Excel 公式解析:按条件去重计数
excel
用户298698530141 天前
告别手动复制:.NET 将网页数据一键导出为 Excel
后端·html·excel
竹林8181 天前
从零到精通:用 Python openpyxl 批量处理 Excel,彻底告别重复劳动
python·excel
diygwcom2 天前
vue3+handsontable实现在线可编辑excel
excel