如何让Spring Data JPA在无事务时保持数据库连接打开?
我有一个Spring Boot 3.0应用,通过Spring Data仓库调用存储过程获取Blob数据,代码如下:
@Service public class CarService { private final CarRepository carRepository; public Set<CarDto> getValuations(LocalDate date) { Blob reportData = carRepository.getReportData("sedan,truck", date); try { byte[] b= reportData.getBytes(1, (int) reportData.length()); log.debug("connection is closed by Spring Data JPA"); //... } // 省略异常处理 } } public interface CarRepository extends Repository<Car, Long> { @Procedure(value="pValuation", outputParameterName="reportData") Blob getReportData(String carTypes, LocalDate tradeDate); }
调用后尝试读取Blob字节时出现错误:
SQLException: com.microsoft.sqlserver.jdbc.SQLServerException: The connection is closed. The connection is closed.
原因是Spring Data在仓库方法返回Blob后就关闭了数据库连接。我可以给getValuations方法标记@Transactional来保持连接打开,但这会引发锁问题导致数据库挂起。
限制条件:
- 不能将
getReportData的返回类型改为byte[],因为数据会被截断至8000字节。 - 存储过程
pValuation是只读的。
更新:
直接使用EntityManager调用存储过程无需事务即可正常运行(读取Blob时连接保持打开),代码如下:
@Service @RequiredArgsConstructor @Slf4j public class CarService { private final CarRepository carRepository; private final EntityManager em; public Set<CarDto> getValuations(LocalDate date) { StoredProcedureQuery q = em.createStoredProcedureQuery("pValuation"); q.registerStoredProcedureParameter("carTypes", String.class, ParameterMode.IN); q.registerStoredProcedureParameter("tradeDate", LocalDate.class, ParameterMode.IN); q.registerStoredProcedureParameter("reportData", Blob.class, ParameterMode.OUT); q.setParameter("carTypes", "sedan,truck"); q.setParameter("tradeDate", date); q.execute(); Blob reportData = (Blob) q.getOutputParameterValue("reportData"); log.debug("got Blob"); try { byte[] b= reportData.getBytes(1, (int) reportData.length()); log.debug("got bytes"); return carRepository.getCarValuations(b); } catch (SQLException convertBlobToBytesException) { log.error(convertBlobToBytesException.toString()); } return Collections.emptySet(); // 补充默认返回 } }
需要解决的问题:如何让Spring Data在无事务的情况下保持连接打开,以便读取Blob的字节数据?
1. 自定义Spring Data仓库实现,在仓库层读取Blob
Spring Data仓库方法执行时,数据库连接处于打开状态,因此可以在仓库的自定义实现类中完成Blob的读取操作,避免返回Blob到Service层时连接已关闭。
步骤如下:
- 定义仓库扩展接口:
public interface CarRepositoryCustom { byte[] getReportDataAsBytes(String carTypes, LocalDate tradeDate); }
- 实现扩展接口:
@Repository public class CarRepositoryImpl implements CarRepositoryCustom { private final EntityManager em; public CarRepositoryImpl(EntityManager em) { this.em = em; } @Override public byte[] getReportDataAsBytes(String carTypes, LocalDate tradeDate) { StoredProcedureQuery q = em.createStoredProcedureQuery("pValuation"); q.registerStoredProcedureParameter("carTypes", String.class, ParameterMode.IN); q.registerStoredProcedureParameter("tradeDate", LocalDate.class, ParameterMode.IN); q.registerStoredProcedureParameter("reportData", Blob.class, ParameterMode.OUT); q.setParameter("carTypes", carTypes); q.setParameter("tradeDate", tradeDate); q.execute(); Blob reportData = (Blob) q.getOutputParameterValue("reportData"); try { return reportData.getBytes(1, (int) reportData.length()); } catch (SQLException e) { throw new RuntimeException("Failed to read Blob data", e); } finally { try { if (reportData != null) { reportData.free(); } } catch (SQLException e) { // 记录清理异常或忽略 } } } }
- 修改原仓库接口继承扩展接口:
public interface CarRepository extends Repository<Car, Long>, CarRepositoryCustom { // 保留其他方法(如果有) }
- Service层调用新方法:
public Set<CarDto> getValuations(LocalDate date) { byte[] reportBytes = carRepository.getReportDataAsBytes("sedan,truck", date); return carRepository.getCarValuations(reportBytes); }
这种方式利用仓库方法执行时的连接上下文,在内部完成Blob读取,无需额外事务,同时避免了连接关闭导致的异常。
2. 使用原生SQL调用存储过程(替代@Procedure)
如果不想自定义仓库实现,可以尝试用原生SQL调用存储过程,确保Blob读取在查询执行过程中完成:
public interface CarRepository extends Repository<Car, Long> { @Query(value = "EXEC pValuation :carTypes, :tradeDate, @reportData OUTPUT", nativeQuery = true) @SqlResultSetMapping( name = "BlobResult", columns = @ColumnResult(name = "reportData", type = Blob.class) ) byte[] getReportData(@Param("carTypes") String carTypes, @Param("tradeDate") LocalDate tradeDate); }
注意:不同数据库的存储过程调用语法有差异,需根据SQL Server的语法调整。
3. 连接池属性调整(不推荐)
可以尝试调整数据源连接池的holdability属性(如HikariCP的相关配置),强制保持连接打开,但这种方式会影响整个应用的连接池行为,灵活性差,不建议优先使用。
最稳妥且符合Spring Data设计思路的方案是自定义仓库实现,在仓库层完成Blob读取,既不需要额外事务,也避免了连接关闭的问题,同时保持代码可维护性。
内容的提问来源于stack exchange,提问作者James

