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"));
}

核心步骤分解

  1. 使用 SqlParameter 定义输入参数 num1num2
  2. 使用 SqlOutParameter 定义输出参数 result
  3. 通过 CallableStatementCreator 创建 CallableStatement 对象
  4. 设置输入参数值,并用 registerOutParameter() 注册输出参数
  5. 从返回的 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 获取。


原始标题:Stored Procedures With Spring JdbcTemplate | Baeldung