bigywater.tech
Java · 366

CallableStatement:调用存储过程的参数边界

CallableStatement 在两个数据库上的行为差别很大:PostgreSQL 把 {call} 翻译成 SELECT,MySQL 会去读过程元数据。这篇用真实的函数、过程与驱动,把这些差别和边界逐个跑出来。

is a procedure
PG 驱动默认模式(escapeSyntaxCallMode=select)下 {call proc(?)} 调用 PROCEDURE 的报错
不支持
PG 驱动 CallableStatement.setInt(String, int) 按名字传参:SQLFeatureNotSupportedException(MySQL 支持)
658 vs 102 µs
MySQL 上每次 prepareCall 与复用同一个 CallableStatement 的单次调用耗时(PG 约 27 µs 两种都一样)

PG:{call} 的三种翻译模式

实测 · 本机执行输出
  ── PostgreSQL 默认(select),调用函数
  {call add_out(3, 4, ?)}:execute() = false;OUT 参数 getInt(3) = 7;getUpdateCount = -1;getMoreResults = false
  INOUT 参数 {call inout_double(?)}:输入 21 → getInt(1) = 42
  函数返回值 {? = call fn_square(?)}:getInt(1) = 81
  名字参数(setInt("a", …)):SQLFeatureNotSupportedException(0A000): Method org.postgresql.jdbc.PgCallableStatement.setInt(String,int) is not yet implemented.
  不 registerOutParameter 就 execute:PSQLException(22023): No value specified for parameter 3.
  getInt 在 registerOutParameter 之后读取不存在的索引 4:PSQLException(2200G): Parameter of type java.sql.Types=0 was registered, but call to getInt (sqltype=java.sql.Types=4) was made.
  OUT 参数登记成错误的类型(VARCHAR)再 getInt:PSQLException(42821): A CallableStatement function was executed and the out parameter 1 was of type java.sql.Types=4 however type java.sql.Types=12 was registered.
  OUT 参数是 NULL 时:getInt = 0;wasNull = true;getObject = null
  参数元数据 getParameterMetaData:PSQLException(42883): ERROR: function add_out(unknown, unknown, unknown) does not exist
  ── PostgreSQL 默认(select),调用过程
  {call add_out(3, 4, ?)}:PSQLException(42883): ERROR: function add_out_p(integer, integer) does not exist
  INOUT 参数 {call inout_double(?)}:PSQLException(42809): ERROR: inout_double_p(integer) is a procedure
  函数返回值 {? = call fn_square(?)}:getInt(1) = 81
  名字参数(setInt("a", …)):SQLFeatureNotSupportedException(0A000): Method org.postgresql.jdbc.PgCallableStatement.setInt(String,int) is not yet implemented.
  不 registerOutParameter 就 execute:PSQLException(22023): No value specified for parameter 3.
  getInt 在 registerOutParameter 之后读取不存在的索引 4:PSQLException(42883): ERROR: function add_out_p(integer, integer) does not exist
  OUT 参数登记成错误的类型(VARCHAR)再 getInt:PSQLException(42883): ERROR: function add_out_p(integer, integer) does not exist
  OUT 参数是 NULL 时:PSQLException(42883): ERROR: function out_null_p() does not exist
  参数元数据 getParameterMetaData:PSQLException(42809): ERROR: add_out_p(unknown, unknown, unknown) is a procedure
  ── PostgreSQL callIfNoReturn,调用过程
  {call add_out(3, 4, ?)}:execute() = false;OUT 参数 getInt(3) = 7;getUpdateCount = -1;getMoreResults = false
  INOUT 参数 {call inout_double(?)}:输入 21 → getInt(1) = 42
  函数返回值 {? = call fn_square(?)}:getInt(1) = 81
  名字参数(setInt("a", …)):SQLFeatureNotSupportedException(0A000): Method org.postgresql.jdbc.PgCallableStatement.setInt(String,int) is not yet implemented.
  不 registerOutParameter 就 execute:PSQLException(22023): No value specified for parameter 3.
  getInt 在 registerOutParameter 之后读取不存在的索引 4:PSQLException(2200G): Parameter of type java.sql.Types=0 was registered, but call to getInt (sqltype=java.sql.Types=4) was made.
  OUT 参数登记成错误的类型(VARCHAR)再 getInt:PSQLException(42821): A CallableStatement function was executed and the out parameter 1 was of type java.sql.Types=4 however type java.sql.Types=12 was registered.
  OUT 参数是 NULL 时:getInt = 0;wasNull = true;getObject = null
  参数元数据 getParameterMetaData:3 个 [1:int4 mode=1][2:int4 mode=1][3:int4 mode=1]
  ── PostgreSQL call,调用过程
  {call add_out(3, 4, ?)}:execute() = false;OUT 参数 getInt(3) = 7;getUpdateCount = -1;getMoreResults = false
  INOUT 参数 {call inout_double(?)}:输入 21 → getInt(1) = 42
  函数返回值 {? = call fn_square(?)}:PSQLException(42883): ERROR: procedure fn_square(unknown, integer) does not exist
  名字参数(setInt("a", …)):SQLFeatureNotSupportedException(0A000): Method org.postgresql.jdbc.PgCallableStatement.setInt(String,int) is not yet implemented.
  不 registerOutParameter 就 execute:PSQLException(22023): No value specified for parameter 3.
  getInt 在 registerOutParameter 之后读取不存在的索引 4:PSQLException(2200G): Parameter of type java.sql.Types=0 was registered, but call to getInt (sqltype=java.sql.Types=4) was made.
  OUT 参数登记成错误的类型(VARCHAR)再 getInt:PSQLException(42821): A CallableStatement function was executed and the out parameter 1 was of type java.sql.Types=4 however type java.sql.Types=12 was registered.
  OUT 参数是 NULL 时:getInt = 0;wasNull = true;getObject = null
  参数元数据 getParameterMetaData:3 个 [1:int4 mode=1][2:int4 mode=1][3:int4 mode=1]
  ── PostgreSQL call,调用函数
  {call add_out(3, 4, ?)}:PSQLException(42809): ERROR: add_out(integer, integer, unknown) is not a procedure
  INOUT 参数 {call inout_double(?)}:PSQLException(42809): ERROR: inout_double(integer) is not a procedure
  函数返回值 {? = call fn_square(?)}:PSQLException(42883): ERROR: procedure fn_square(unknown, integer) does not exist
  名字参数(setInt("a", …)):SQLFeatureNotSupportedException(0A000): Method org.postgresql.jdbc.PgCallableStatement.setInt(String,int) is not yet implemented.
  不 registerOutParameter 就 execute:PSQLException(22023): No value specified for parameter 3.
  getInt 在 registerOutParameter 之后读取不存在的索引 4:PSQLException(42809): ERROR: add_out(integer, integer, unknown) is not a procedure
  OUT 参数登记成错误的类型(VARCHAR)再 getInt:PSQLException(42809): ERROR: add_out(integer, integer, unknown) is not a procedure
  OUT 参数是 NULL 时:PSQLException(42809): ERROR: out_null(unknown) is not a procedure
  参数元数据 getParameterMetaData:PSQLException(42809): ERROR: add_out(unknown, unknown, unknown) is not a procedure

MySQL 的 IN / OUT / INOUT

实测 · 本机执行输出
  ── MySQL
  {call add_out(3, 4, ?)}:execute() = false;OUT 参数 getInt(3) = 7;getUpdateCount = 0;getMoreResults = false
  INOUT 参数 {call inout_double(?)}:输入 21 → getInt(1) = 42
  函数返回值 {? = call fn_square(?)}:getInt(1) = 81
  名字参数(setInt("a", …)):OUT r = 7
  不 registerOutParameter 就 execute:execute 成功;getInt(3) = 3
  getInt 在 registerOutParameter 之后读取不存在的索引 4:SQLException(S1009): Parameter index of 4 is out of range (1, 3)
  OUT 参数登记成错误的类型(VARCHAR)再 getInt:getString = 3;getInt = 3
  OUT 参数是 NULL 时:getInt = 0;wasNull = true;getObject = null
  参数元数据 getParameterMetaData:3 个 [1:INT mode=1][2:INT mode=1][3:INT mode=4]

结果集、更新计数与 refcursor

实测 · 本机执行输出
  ── PostgreSQL(默认)
  {call multi_rs(2)}:[结果集 1:1 列 2 行]
  PG 函数返回 refcursor(autoCommit = false):读取到 4 行
  同样的 refcursor 在 autoCommit = true 下:PSQLException(34000): ERROR: cursor "mycur" does not exist
  {call multi_rs_proc(?)}(过程,INOUT):PSQLException(42809): ERROR: multi_rs_proc(integer) is a procedure
  {call fn_square(?)}(函数,不用 ? =):execute()=true;结果集第 1 行 = 81
  原生 CALL add_out_p(?, ?, ?):OUT = 7
  ── PostgreSQL(call 模式,过程)
  {call multi_rs(2)}:PSQLException(42809): ERROR: multi_rs(integer) is not a procedure
  PG 函数返回 refcursor(autoCommit = false):PSQLException(42883): ERROR: procedure fn_cursor(unknown) does not exist
  同样的 refcursor 在 autoCommit = true 下:PSQLException(42883): ERROR: procedure fn_cursor(unknown) does not exist
  {call multi_rs_proc(?)}(过程,INOUT):getInt(1) = 3
  {call fn_square(?)}(函数,不用 ? =):PSQLException(42809): ERROR: fn_square(integer) is not a procedure
  原生 CALL add_out_p(?, ?, ?):OUT = 7
  ── MySQL
  {call multi_rs(2)}:[结果集 1:2 列 1 行][结果集 2:1 列 1 行][更新计数 0]
  MySQL 既返回结果集又有 OUT 参数:结果集 1 行;读 OUT 参数 getInt(2) = 500
  没有先读完结果集就读 OUT 参数:500

错误、批处理与事务

实测 · 本机执行输出
  ── PostgreSQL(默认,函数)
  调用不存在的过程:PSQLException(42883): ERROR: function no_such_proc(integer) does not exist
  参数个数不对(只传 2 个):成功
  过程里 RAISE / SIGNAL 异常:PSQLException(45000): ERROR: boom 1
  没有设置 IN 参数:PSQLException(22023): No value specified for parameter 2.
  registerOutParameter 的 JDBC 类型与数据库类型不匹配(INTEGER 过程登记为 TIMESTAMP):PSQLException(42821): A CallableStatement function was executed and the out parameter 1 was of type java.sql.Types=4 however type java.sql.Types=93 was registered.
  重复使用同一个 CallableStatement 执行多次:10 11 12
  executeBatch 调用存储过程:executeBatch → [0, 0, 0];log 表 3 行
  在 autoCommit = false 下过程内部出错后是否可继续:PSQLException(25P02): ERROR: current transaction is aborted, commands ignored until end of transaction block
  ── PostgreSQL(call 模式,过程)
  调用不存在的过程:PSQLException(42883): ERROR: procedure no_such_proc(integer) does not exist
  参数个数不对(只传 2 个):PSQLException(42883): ERROR: procedure add_out_p(integer, integer) does not exist
  过程里 RAISE / SIGNAL 异常:PSQLException(45000): ERROR: boom 1
  没有设置 IN 参数:PSQLException(22023): No value specified for parameter 2.
  registerOutParameter 的 JDBC 类型与数据库类型不匹配(INTEGER 过程登记为 TIMESTAMP):PSQLException(42821): A CallableStatement function was executed and the out parameter 1 was of type java.sql.Types=4 however type java.sql.Types=93 was registered.
  重复使用同一个 CallableStatement 执行多次:10 11 12
  executeBatch 调用存储过程:executeBatch → [-1, -1, -1];log 表 6 行
  在 autoCommit = false 下过程内部出错后是否可继续:PSQLException(25P02): ERROR: current transaction is aborted, commands ignored until end of transaction block
  ── MySQL
  调用不存在的过程:SQLSyntaxErrorException(42000): PROCEDURE lab.no_such_proc does not exist
  参数个数不对(只传 2 个):SQLException(S1009): Parameter r is not registered as an output parameter
  过程里 RAISE / SIGNAL 异常:SQLException(45000): boom
  没有设置 IN 参数:SQLException(07001): No value specified for parameter 2
  registerOutParameter 的 JDBC 类型与数据库类型不匹配(INTEGER 过程登记为 TIMESTAMP):SQLDataException(22018): Unsupported conversion from LONG to java.sql.Timestamp
  重复使用同一个 CallableStatement 执行多次:10 11 12
  executeBatch 调用存储过程:executeBatch → [1, 1, 1];log 表 3 行
  在 autoCommit = false 下过程内部出错后是否可继续:后续查询 → 1

调用开销

实测 · 本机执行输出
  ── PostgreSQL(默认,函数)
  3000 次调用(同一连接):每次 prepareCall 27 µs;复用同一个 CallableStatement 26 µs;同样逻辑的 PreparedStatement 查询 25 µs
  ── PostgreSQL(call 模式,过程)
  3000 次调用(同一连接):每次 prepareCall 31 µs;复用同一个 CallableStatement 30 µs;同样逻辑的 PreparedStatement 查询 25 µs
  ── MySQL
  3000 次调用(同一连接):每次 prepareCall 658 µs;复用同一个 CallableStatement 102 µs;同样逻辑的 PreparedStatement 查询 29 µs

分步演示

CallableStatement
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64,JDK 21.0.12;PostgreSQL 16.15 与 MySQL 26.7.0(都是自己初始化的隔离实例,端口 9501 / 9601);驱动 PostgreSQL JDBC 42.7.4、MySQL Connector/J 9.1.0;PG 实验里的函数与过程是为实验创建的,过程以 _p 结尾
    • 没有验证:Oracle 的 PL/SQL 与 SQL Server 的存储过程(含 RETURN_VALUE 与表值参数)、MySQL 每次 prepareCall 慢的具体原因(可能是读取参数元数据,未验证)、PG 的 escapeSyntaxCallMode 对 getParameterMetaData 的完整影响、存储过程里的多语句事务控制(COMMIT / ROLLBACK 在过程内)、并发调用同一个 CallableStatement 的线程安全
    京ICP备2026067502号-1京公网安备11010502063487号