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

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

本文档总结配置导入(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

相关推荐
写了20年代码的老程序员10 小时前
Excel 导入导出为什么总是把后端逼成字段搬运工
java·excel
Cloud_Shy61810 小时前
Python 数据分析基础入门:《Excel Python:飞速搞定数据分析与处理》学习笔记系列(第十一章 Python 包跟踪器 中篇)
数据库·python·sql·数据分析·excel·web
Metaphor69212 小时前
使用 Python 将 Excel 转换为 PDF
python·pdf·excel
星越华夏14 小时前
Pandas获取excel表sheet名称
excel·pandas
俊哥工具15 小时前
不用安装不收费!多功能U盘修复工具,解决大部分U盘故障
学习·pdf·word·excel·音视频
_oP_i16 小时前
Excel 工作簿取消保护
excel
程序员杰哥16 小时前
Python+requests+excel 接口自动化测试框架
自动化测试·软件测试·python·测试工具·测试用例·excel·接口测试
流形填表1 天前
大风车Excel|本地版软件下载与使用教程(2026最新版)
excel
流形填表2 天前
大风车Excel|2026年最新消息
excel
Cloud_Shy6182 天前
Python 数据分析基础入门:《Excel Python:飞速搞定数据分析与处理》学习笔记系列(第十一章 Python 包跟踪器 上篇)
python·数据分析·excel·pandas·matplotlib