作为一名长期奋战在一线的Java开发者,我深知JDBC操作数据库是每个后端工程师的必修课。今天我想和大家分享一个在实际项目中经常遇到但容易被忽视的技术点——通过JDBC调用MySQL存储过程和存储函数的完整实践方案。这个技能在复杂业务场景下尤为重要,比如需要处理事务性操作、批量数据处理时,存储过程能大幅提升效率。
在开始具体实践前,我们需要先理清JDBC中三种核心Statement对象的区别:
Statement:最基础的SQL执行接口
PreparedStatement:预编译Statement
CallableStatement:专用于存储过程/函数调用
提示:在MySQL Connector/J驱动中,CallableStatement实际上是通过PreparedStatement实现的,所以它们的性能特征相似。
为什么我们需要专门使用CallableStatement来调用存储过程?主要基于以下几点考虑:
这是最简单的场景,我们先创建一个基础存储过程:
sql复制CREATE PROCEDURE get_all_employees()
BEGIN
SELECT * FROM employees;
END;
对应的Java调用代码:
java复制public class NoParamProcedureDemo {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/company";
String user = "root";
String password = "123456";
try (Connection conn = DriverManager.getConnection(url, user, password);
CallableStatement cstmt = conn.prepareCall("{call get_all_employees()}")) {
boolean hasResults = cstmt.execute();
if (hasResults) {
try (ResultSet rs = cstmt.getResultSet()) {
ResultSetMetaData meta = rs.getMetaData();
int colCount = meta.getColumnCount();
while (rs.next()) {
for (int i = 1; i <= colCount; i++) {
System.out.print(rs.getString(i) + "\t");
}
System.out.println();
}
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
关键点说明:
{call procedure_name()}语法格式实际业务中,带参数的存储过程更为常见。我们先创建一个处理商品销售的存储过程:
sql复制CREATE PROCEDURE process_sale(
IN product_id VARCHAR(20),
IN quantity INT,
IN customer_id INT
)
BEGIN
START TRANSACTION;
UPDATE inventory SET stock = stock - quantity WHERE id = product_id;
INSERT INTO sales (product_id, customer_id, quantity, sale_date)
VALUES (product_id, customer_id, quantity, NOW());
COMMIT;
END;
Java调用代码:
java复制public class InParamProcedureDemo {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/store";
String user = "root";
String password = "123456";
try (Connection conn = DriverManager.getConnection(url, user, password);
CallableStatement cstmt = conn.prepareCall("{call process_sale(?, ?, ?)}")) {
// 设置输入参数
cstmt.setString(1, "P1001");
cstmt.setInt(2, 5);
cstmt.setInt(3, 1024);
boolean success = cstmt.execute();
System.out.println("Sale processed: " + (success ? "success" : "failure"));
} catch (SQLException e) {
e.printStackTrace();
}
}
}
注意事项:
?的位置与存储过程参数定义顺序一致OUT参数允许存储过程返回计算结果,这是存储过程的重要特性。创建示例:
sql复制CREATE PROCEDURE calculate_stats(
IN department_id INT,
OUT employee_count INT,
OUT avg_salary DECIMAL(10,2)
)
BEGIN
SELECT COUNT(*), AVG(salary)
INTO employee_count, avg_salary
FROM employees
WHERE dept_id = department_id;
END;
Java调用代码:
java复制public class OutParamProcedureDemo {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/company";
String user = "root";
String password = "123456";
try (Connection conn = DriverManager.getConnection(url, user, password);
CallableStatement cstmt = conn.prepareCall("{call calculate_stats(?, ?, ?)}")) {
// 设置输入参数
cstmt.setInt(1, 101);
// 注册输出参数
cstmt.registerOutParameter(2, Types.INTEGER);
cstmt.registerOutParameter(3, Types.DECIMAL);
cstmt.execute();
// 获取输出参数值
int count = cstmt.getInt(2);
BigDecimal avgSalary = cstmt.getBigDecimal(3);
System.out.printf("Department 101 has %d employees with average salary %.2f%n",
count, avgSalary);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
关键点:
INOUT参数兼具输入输出功能,典型应用是计数器场景:
sql复制CREATE PROCEDURE increment_counter(
INOUT counter INT,
IN increment INT
)
BEGIN
SET counter = counter + increment;
END;
Java调用代码:
java复制public class InOutParamDemo {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "123456";
try (Connection conn = DriverManager.getConnection(url, user, password);
CallableStatement cstmt = conn.prepareCall("{call increment_counter(?, ?)}")) {
int initialValue = 10;
int increment = 5;
// 设置输入值
cstmt.setInt(1, initialValue);
cstmt.setInt(2, increment);
// 注册INOUT参数
cstmt.registerOutParameter(1, Types.INTEGER);
cstmt.execute();
int result = cstmt.getInt(1);
System.out.println("Result: " + result); // 输出15
} catch (SQLException e) {
e.printStackTrace();
}
}
}
特殊处理要点:
存储过程可能返回多个结果集,这是与普通查询的重要区别。示例存储过程:
sql复制CREATE PROCEDURE get_employee_details(IN emp_id INT)
BEGIN
SELECT * FROM employees WHERE id = emp_id;
SELECT * FROM salaries WHERE employee_id = emp_id;
SELECT * FROM performance_reviews WHERE employee_id = emp_id;
END;
Java处理代码:
java复制public class MultipleResultSetDemo {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/company";
String user = "root";
String password = "123456";
try (Connection conn = DriverManager.getConnection(url, user, password);
CallableStatement cstmt = conn.prepareCall("{call get_employee_details(?)}")) {
cstmt.setInt(1, 1001);
boolean hasResults = cstmt.execute();
int resultSetCount = 0;
do {
if (hasResults) {
try (ResultSet rs = cstmt.getResultSet()) {
resultSetCount++;
System.out.println("----- Result Set " + resultSetCount + " -----");
ResultSetMetaData meta = rs.getMetaData();
int colCount = meta.getColumnCount();
// 打印列名
for (int i = 1; i <= colCount; i++) {
System.out.print(meta.getColumnName(i) + "\t");
}
System.out.println();
// 打印数据
while (rs.next()) {
for (int i = 1; i <= colCount; i++) {
System.out.print(rs.getString(i) + "\t");
}
System.out.println();
}
}
}
hasResults = cstmt.getMoreResults();
} while (hasResults || cstmt.getUpdateCount() != -1);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
处理要点:
存储函数与存储过程类似,但语法上有区别。创建示例函数:
sql复制CREATE FUNCTION get_department_name(dept_id INT)
RETURNS VARCHAR(50)
DETERMINISTIC
BEGIN
DECLARE dept_name VARCHAR(50);
SELECT name INTO dept_name FROM departments WHERE id = dept_id;
RETURN dept_name;
END;
Java调用代码:
java复制public class FunctionCallDemo {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/company";
String user = "root";
String password = "123456";
try (Connection conn = DriverManager.getConnection(url, user, password);
CallableStatement cstmt = conn.prepareCall("{? = call get_department_name(?)}")) {
// 注册返回值
cstmt.registerOutParameter(1, Types.VARCHAR);
// 设置输入参数
cstmt.setInt(2, 101);
cstmt.execute();
String deptName = cstmt.getString(1);
System.out.println("Department name: " + deptName);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
关键区别:
{? = call function_name(?)}创建CallableStatement时可以指定结果集特性:
java复制// 创建可滚动、只读的结果集
CallableStatement cstmt = conn.prepareCall(
"{call get_employees()}",
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY
);
// 创建可更新的结果集
CallableStatement cstmt = conn.prepareCall(
"{call get_employees_for_update()}",
ResultSet.TYPE_SCROLL_SENSITIVE,
ResultSet.CONCUR_UPDATABLE
);
适用场景:
对于需要重复调用的存储过程,可以使用批量处理提升性能:
java复制public class BatchProcedureDemo {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/order_system";
String user = "root";
String password = "123456";
try (Connection conn = DriverManager.getConnection(url, user, password);
CallableStatement cstmt = conn.prepareCall("{call process_order(?, ?)}")) {
// 禁用自动提交
conn.setAutoCommit(false);
// 添加批量操作
for (int i = 0; i < 100; i++) {
cstmt.setString(1, "ORDER_" + i);
cstmt.setInt(2, i % 5 + 1);
cstmt.addBatch();
if (i % 20 == 0) {
cstmt.executeBatch();
}
}
// 执行剩余批次
cstmt.executeBatch();
conn.commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
性能优化点:
参数类型不匹配
忘记注册OUT参数
结果集未关闭
多结果集处理不完整
启用JDBC日志
java复制DriverManager.setLogWriter(new PrintWriter(System.out));
检查存储过程定义
sql复制SHOW CREATE PROCEDURE procedure_name;
单独测试SQL
先在MySQL客户端测试存储过程逻辑
连接池配置
预编译重用
结果集处理优化
java复制// 流式结果集示例
CallableStatement cstmt = conn.prepareCall(
"{call get_large_dataset()}",
ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY
);
cstmt.setFetchSize(Integer.MIN_VALUE); // 启用流式模式
虽然JDBC是标准API,但在现代Java生态中,我们有更多选择:
java复制@NamedStoredProcedureQuery(
name = "calculateStats",
procedureName = "calculate_stats",
parameters = {
@StoredProcedureParameter(name = "deptId", mode = ParameterMode.IN, type = Integer.class),
@StoredProcedureParameter(name = "empCount", mode = ParameterMode.OUT, type = Integer.class),
@StoredProcedureParameter(name = "avgSalary", mode = ParameterMode.OUT, type = Double.class)
}
)
// 调用方式
StoredProcedureQuery query = em.createNamedStoredProcedureQuery("calculateStats");
query.setParameter("deptId", 101);
query.execute();
int count = (Integer) query.getOutputParameterValue("empCount");
java复制@Repository
public class EmployeeRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
public int getEmployeeCount(int deptId) {
return jdbcTemplate.execute(
conn -> {
CallableStatement cs = conn.prepareCall("{? = call get_employee_count(?)}");
cs.registerOutParameter(1, Types.INTEGER);
cs.setInt(2, deptId);
cs.execute();
return cs.getInt(1);
}
);
}
}
Mapper接口:
java复制public interface EmployeeMapper {
@Options(statementType = StatementType.CALLABLE)
@Select("{call get_employee_details(#{id, mode=IN}, #{name, mode=OUT, jdbcType=VARCHAR})}")
void getEmployeeDetails(@Param("id") int id, @Param("name") String name);
}
调用方式:
java复制String name = null;
employeeMapper.getEmployeeDetails(1001, name);
System.out.println("Employee name: " + name);
在实际项目开发中,我通常会根据项目架构选择最合适的方案。对于纯JDBC项目,掌握原生CallableStatement的使用至关重要;而在Spring生态中,JdbcTemplate或JPA的方案更为简洁高效。