1. 概述
本文将探讨 Spring JDBC 框架中 JdbcTemplate
类调用数据库存储过程的能力。存储过程类似于函数,但存在关键区别:函数仅支持输入参数且有返回类型,而存储过程同时支持输入和输出参数。
2. 前置条件
以 PostgreSQL 数据库中的简单存储过程为例:
CREATE OR REPLACE PROCEDURE sum_two_numbers(
IN num1 INTEGER,
IN num2 INTEGER,
OUT result INTEGER
)
LANGUAGE plpgsql
AS '
BEGIN
sum_result := num1 + num2;
END;
';
存储过程 sum_two_numbers
接收两个输入参数,并通过输出参数 sum_result
返回它们的和。虽然存储过程通常支持多个输入/输出参数,但本示例仅使用单个输出参数。
3. 使用 JdbcTemplate#call()
方法
通过 JdbcTemplate#call()
方法调用存储过程:
void givenStoredProc_whenCallableStatement_thenExecProcUsingJdbcTemplateCallMethod() {
List<SqlParameter> procedureParams = List.of(new SqlParameter("num1", Types.INTEGER),
new SqlParameter("num2", Types.NUMERIC),
new SqlOutParameter("result", Types.NUMERIC)
);
Map<String, Object> resultMap = jdbcTemplate.call(new CallableStatementCreator() {
@Override
public CallableStatement createCallableStatement(Connection con) throws SQLException {
CallableStatement callableStatement = con.prepareCall("call sum_two_numbers(?, ?, ?)");
callableStatement.registerOutParameter(3, Types.NUMERIC);
callableStatement.setInt(1, 4);
callableStatement.setInt(2, 5);
return callableStatement;
}
}, procedureParams);
assertEquals(new BigDecimal(9), resultMap.get("result"));
}
核心步骤分解:
- 使用
SqlParameter
定义输入参数num1
和num2
- 使用
SqlOutParameter
定义输出参数result
- 通过
CallableStatementCreator
创建CallableStatement
对象 - 设置输入参数值,并用
registerOutParameter()
注册输出参数 - 从返回的
Map
中获取结果
⚠️ 注意:输出参数必须显式注册,否则会抛出异常。
4. 使用 JdbcTemplate#execute()
方法
当需要更精细控制 CallableStatement
时,可使用 CallableStatementCallback
接口:
void givenStoredProc_whenCallableStatement_thenExecProcUsingJdbcTemplateExecuteMethod() {
String command = jdbcTemplate.execute(new CallableStatementCreator() {
@Override
public CallableStatement createCallableStatement(Connection con) throws SQLException {
CallableStatement callableStatement = con.prepareCall("call sum_two_numbers(?, ?, ?)");
return callableStatement;
}
}, new CallableStatementCallback<String>() {
@Override
public String doInCallableStatement(CallableStatement cs) throws SQLException, DataAccessException {
cs.setInt(1, 4);
cs.setInt(2, 5);
cs.registerOutParameter(3, Types.NUMERIC);
cs.execute();
BigDecimal result = cs.getBigDecimal(3);
assertEquals(new BigDecimal(9), result);
String command = "4 + 5 = " + cs.getBigDecimal(3);
return command;
}
});
assertEquals("4 + 5 = 9", command);
}
关键优势:
- 在
doInCallableStatement()
方法中复用CallableStatement
对象 - 支持多次执行同一存储过程(使用不同参数)
- 可在回调中处理复杂业务逻辑
✅ 适合需要参数预处理或结果后处理的场景。
5. 使用 SimpleJdbcCall
SimpleJdbcCall
类通过 JdbcTemplate
简化存储过程调用:
void givenStoredProc_whenJdbcTemplate_thenCreateSimpleJdbcCallAndExecProc() {
SimpleJdbcCall simpleJdbcCall = new SimpleJdbcCall(jdbcTemplate).withProcedureName("sum_two_numbers");
Map<String, Integer> inParams = new HashMap<>();
inParams.put("num1", 4);
inParams.put("num2", 5);
Map<String, Object> resultMap = simpleJdbcCall.execute(inParams);
assertEquals(new BigDecimal(9), resultMap.get("result"));
}
特性对比: | 特性 | 描述 | |--------------------|----------------------------------------------------------------------| | 元数据自动读取 | 支持从数据库自动获取参数信息(PostgreSQL/MySQL/Oracle等主流数据库) | | 线程安全 | 设计支持多线程并发调用 | | 链式调用 | 流畅的API风格,代码简洁 |
对于不支持元数据读取的数据库,需显式声明参数:
@Test
void givenStoredProc_whenJdbcTemplateAndDisableMetadata_thenCreateSimpleJdbcCallAndExecProc() {
SimpleJdbcCall simpleJdbcCall = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("sum_two_numbers")
.withoutProcedureColumnMetaDataAccess();
simpleJdbcCall.declareParameters(new SqlParameter("num1", Types.NUMERIC),
new SqlParameter("num2", Types.NUMERIC),
new SqlOutParameter("result", Types.NUMERIC));
Map<String, Integer> inParams = new HashMap<>();
inParams.put("num1", 4);
inParams.put("num2", 5);
Map<String, Object> resultMap = simpleJdbcCall.execute(inParams);
assertEquals(new BigDecimal(9), resultMap.get("result"));
}
❌ 数据库不支持的元数据类型时,必须手动调用 declareParameters()
。
6. 使用 StoredProcedure
StoredProcedure
是抽象类,适合需要扩展处理的场景:
public class StoredProcedureImpl extends StoredProcedure {
public StoredProcedureImpl(JdbcTemplate jdbcTemplate, String procName) {
super(jdbcTemplate, procName);
}
private String doSomeProcess(Object procName) {
//业务逻辑处理
return null;
}
@Override
public Map<String, Object> execute(Map<String, ?> inParams) throws DataAccessException {
doSomeProcess(inParams);
return super.execute(inParams);
}
}
使用示例:
@Test
void givenStoredProc_whenJdbcTemplate_thenCreateStoredProcedureAndExecProc() {
StoredProcedure storedProcedure = new StoredProcedureImpl(jdbcTemplate, "sum_two_numbers");
storedProcedure.declareParameter(new SqlParameter("num1", Types.NUMERIC));
storedProcedure.declareParameter(new SqlParameter("num2", Types.NUMERIC));
storedProcedure.declareParameter(new SqlOutParameter("result", Types.NUMERIC));
Map<String, Integer> inParams = new HashMap<>();
inParams.put("num1", 4);
inParams.put("num2", 5);
Map<String, Object> resultMap = storedProcedure.execute(inParams);
assertEquals(new BigDecimal(9), resultMap.get("result"));
}
⚠️ 严格踩坑点:参数声明顺序必须与存储过程定义一致,否则会抛出参数不匹配异常。
7. 总结
Spring 提供了四种调用存储过程的方式,适用场景对比:
方式 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
JdbcTemplate#call() |
简单参数场景 | 直接控制 CallableStatement |
代码冗长 |
JdbcTemplate#execute() |
需要自定义处理逻辑 | 灵活性高,支持回调扩展 | 实现复杂 |
SimpleJdbcCall |
主流数据库(支持元数据) | 代码简洁,线程安全 | 部分数据库需手动声明参数 |
StoredProcedure |
需要继承扩展的业务场景 | 支持预处理/后处理逻辑 | 需创建子类 |
最佳实践建议:
- 优先使用
SimpleJdbcCall
(代码简洁性最优) - 需要复杂处理时选择
StoredProcedure
(扩展性最强) - 避免在简单场景中使用
JdbcTemplate#call()
(代码可读性差)
完整代码示例可在 GitHub 获取。