目录
- 准备工作
- 调用存储过程的几种主要场景
- 调用无参数、无返回值的存储过程
- 调用带
IN参数的存储过程 - 调用带
OUT参数的存储过程 - 调用带
INOUT参数的存储过程 - 调用返回结果集的存储过程
- 最佳实践与注意事项
- 完整示例代码
准备工作
在开始之前,请确保你已经完成了以下配置:

a. MySQL 存储过程示例
为了演示,我们先在 MySQL 数据库中创建几个不同类型的存储过程。
-- 创建一个测试数据库(如果不存在)
CREATE DATABASE IF NOT EXISTS `test_db` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `test_db`;
-- 创建一个用于演示的表
CREATE TABLE `users` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(50) NOT NULL,
`email` VARCHAR(100) NOT NULL
);
-- 插入一些测试数据
INSERT INTO `users` (`name`, `email`) VALUES
('Alice', 'alice@example.com'),
('Bob', 'bob@example.com'),
('Charlie', 'charlie@example.com');
创建我们需要的存储过程:
-- 场景一:无参数、无返回值,仅执行操作
DELIMITER //
CREATE PROCEDURE `add_user`(IN p_name VARCHAR(50), IN p_email VARCHAR(100))
BEGIN
INSERT INTO users (name, email) VALUES (p_name, p_email);
END //
DELIMITER ;
-- 场景二:带OUT参数,返回一个值
DELIMITER //
CREATE PROCEDURE `get_user_count`(OUT p_count INT)
BEGIN
SELECT COUNT(*) INTO p_count FROM users;
END //
DELIMITER ;
-- 场景三:带INOUT参数,修改传入的值
DELIMITER //
CREATE PROCEDURE `format_name`(INOUT p_name VARCHAR(100))
BEGIN
SET p_name = CONCAT('User: ', UPPER(p_name));
END //
DELIMITER ;
-- 场景四:返回一个结果集
DELIMITER //
CREATE PROCEDURE `get_all_users`()
BEGIN
SELECT id, name, email FROM users;
END //
DELIMITER ;
b. Java 项目配置
你需要一个 MySQL 的 JDBC 驱动,如果你使用 Maven,在 pom.xml 中添加依赖:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version> <!-- 建议使用较新版本 -->
</dependency>
c. 数据库连接信息
准备好你的数据库连接信息:

- URL:
jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC - User:
your_username - Password:
your_password
调用存储过程的几种主要场景
在 Java 中,我们使用 java.sql.CallableStatement 来调用存储过程,它类似于 PreparedStatement,但专门用于处理 SQL 存储过程和函数。
调用无参数、无返回值的存储过程
例如调用我们上面创建的 add_user 存储过程。
Java 代码:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.CallableStatement;
public class CallSimpleProcedure {
private static final String DB_URL = "jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC";
private static final String USER = "root"; // 你的用户名
private static final String PASS = "password"; // 你的密码
public static void main(String[] args) {
Connection conn = null;
CallableStatement cstmt = null;
try {
// 1. 加载驱动 (对于新版本驱动,可以省略此步)
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 获取数据库连接
conn = DriverManager.getConnection(DB_URL, USER, PASS);
// 3. 创建 CallableStatement 对象
// 语法: {call procedure_name()}
String sql = "{call add_user(?, ?)}";
cstmt = conn.prepareCall(sql);
// 4. 设置 IN 参数
cstmt.setString(1, "David");
cstmt.setString(2, "david@example.com");
// 5. 执行存储过程
cstmt.execute();
System.out.println("成功调用 add_user 存储过程,新用户已添加。");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6. 关闭资源
try {
if (cstmt != null) cstmt.close();
if (conn != null) conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
调用带 IN 参数的存储过程
这与场景一类似,add_user 本身就是带 IN 参数的,上面的代码已经展示了如何设置 IN 参数。
调用带 OUT 参数的存储过程
例如调用 get_user_count 存储过程来获取用户总数。
关键点:
- 在 SQL 字符串中,用 标记
OUT参数。 - 在执行前,必须使用
registerOutParameter(index, sqlType)注册OUT参数。 - 执行后,通过
getXXX(index)方法获取OUT参数的值。
Java 代码:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.CallableStatement;
public class CallProcedureWithOutParam {
private static final String DB_URL = "jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC";
private static final String USER = "root";
private static final String PASS = "password";
public static void main(String[] args) {
Connection conn = null;
CallableStatement cstmt = null;
try {
conn = DriverManager.getConnection(DB_URL, USER, PASS);
// 语法: {call procedure_name(?)},? 代表 OUT 参数
String sql = "{call get_user_count(?)}";
cstmt = conn.prepareCall(sql);
// 注册 OUT 参数 (索引从 1 开始),类型为 INTEGER
cstmt.registerOutParameter(1, java.sql.Types.INTEGER);
// 执行
cstmt.execute();
// 获取 OUT 参数的值
int userCount = cstmt.getInt(1);
System.out.println("当前用户总数为: " + userCount);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源...
try { if (cstmt != null) cstmt.close(); } catch (Exception e) {}
try { if (conn != null) conn.close(); } catch (Exception e) {}
}
}
}
调用带 INOUT 参数的存储过程
例如调用 format_name 存储过程。
关键点:
INOUT参数也需要注册。- 必须先设置
IN值,再注册INOUT参数,或者按顺序执行。
Java 代码:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.CallableStatement;
public class CallProcedureWithInOutParam {
private static final String DB_URL = "jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC";
private static final String USER = "root";
private static final String PASS = "password";
public static void main(String[] args) {
Connection conn = null;
CallableStatement cstmt = null;
try {
conn = DriverManager.getConnection(DB_URL, USER, PASS);
// 语法: {call procedure_name(?)},? 代表 INOUT 参数
String sql = "{call format_name(?)}";
cstmt = conn.prepareCall(sql);
// 1. 设置 IN 值
String originalName = "Eve";
cstmt.setString(1, originalName);
// 2. 注册 INOUT 参数 (类型为 VARCHAR)
cstmt.registerOutParameter(1, java.sql.Types.VARCHAR);
// 3. 执行
cstmt.execute();
// 4. 获取返回的 OUT 值
String formattedName = cstmt.getString(1);
System.out.println("原始名称: " + originalName);
System.out.println("格式化后名称: " + formattedName);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源...
try { if (cstmt != null) cstmt.close(); } catch (Exception e) {}
try { if (conn != null) conn.close(); } catch (Exception e) {}
}
}
}
调用返回结果集的存储过程
例如调用 get_all_users 存储过程。
关键点:
- 使用
cstmt.execute()执行。 - 使用
cstmt.getResultSet()来获取返回的结果集。 getResultSet()只能获取第一个结果集,如果存储过程返回多个结果集,需要循环使用cstmt.getMoreResults()并调用cstmt.getResultSet()。
Java 代码:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.CallableStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class CallProcedureWithResultSet {
private static final String DB_URL = "jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC";
private static final String USER = "root";
private static final String PASS = "password";
public static void main(String[] args) {
Connection conn = null;
CallableStatement cstmt = null;
ResultSet rs = null;
try {
conn = DriverManager.getConnection(DB_URL, USER, PASS);
// 语法: {call procedure_name()}
String sql = "{call get_all_users()}";
cstmt = conn.prepareCall(sql);
// 执行
cstmt.execute();
// 获取结果集
rs = cstmt.getResultSet();
System.out.println("所有用户信息:");
System.out.println("ID | Name | Email");
System.out.println("---------------------------");
// 遍历结果集
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
String email = rs.getString("email");
System.out.printf("%d | %-10s | %s\n", id, name, email);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源 (顺序很重要: ResultSet -> Statement -> Connection)
try { if (rs != null) rs.close(); } catch (SQLException e) {}
try { if (cstmt != null) cstmt.close(); } catch (SQLException e) {}
try { if (conn != null) conn.close(); } catch (SQLException e) {}
}
}
}
最佳实践与注意事项
-
关闭资源:数据库连接 (
Connection)、语句 (Statement/PreparedStatement/CallableStatement) 和结果集 (ResultSet) 都是有限资源。必须在finally块中关闭它们,以防止资源泄漏,关闭顺序是:ResultSet->Statement->Connection。 -
使用 try-with-resources (Java 7+):这是更推荐的方式,它可以自动关闭实现了
AutoCloseable接口(如Connection,Statement,ResultSet)的资源,代码更简洁、安全。try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); CallableStatement cstmt = conn.prepareCall(sql)) { // ... 执行逻辑 ... } catch (SQLException e) { e.printStackTrace(); } // conn 和 cstmt 会在这里自动关闭 -
处理异常:始终妥善处理
SQLException,不要简单地printStackTrace(),在生产环境中应该记录日志或向用户展示友好的错误信息。 -
防止 SQL 注入:虽然存储过程本身可以减少 SQL 注入的风险,但当你动态拼接存储过程的名称时,仍然存在风险,如果需要动态调用,请确保对存储过程名称进行严格的校验。
-
参数索引:
CallableStatement中的参数索引从1开始,而不是0,这一点很容易出错。 -
明确参数类型:在注册
OUT或INOUT参数时,必须明确指定其 SQL 类型(如java.sql.Types.INTEGER,java.sql.Types.VARCHAR),以确保数据类型转换的正确性。
完整示例代码(整合版)
下面是一个整合了 try-with-resources 和多种调用方式的完整示例。
import java.sql.*;
import java.util.Scanner;
public class ComprehensiveJdbcExample {
private static final String DB_URL = "jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC";
private static final String USER = "root";
private static final String PASS = "password";
public static void main(String[] args) {
// 场景一和二:添加用户 (带IN参数)
addUser("Frank", "frank@example.com");
// 场景三:获取用户总数 (带OUT参数)
getUserCount();
// 场景四:格式化名称 (带INOUT参数)
formatName("Grace");
// 场景五:获取所有用户 (返回结果集)
getAllUsers();
}
public static void addUser(String name, String email) {
// SQL 中的 ? 占位符会自动按顺序匹配 IN 参数
String sql = "{call add_user(?, ?)}";
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
CallableStatement cstmt = conn.prepareCall(sql)) {
cstmt.setString(1, name);
cstmt.setString(2, email);
cstmt.execute();
System.out.println("成功添加用户: " + name);
} catch (SQLException e) {
System.err.println("添加用户失败: " + e.getMessage());
}
}
public static void getUserCount() {
String sql = "{call get_user_count(?)}";
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
CallableStatement cstmt = conn.prepareCall(sql)) {
cstmt.registerOutParameter(1, Types.INTEGER);
cstmt.execute();
int count = cstmt.getInt(1);
System.out.println("当前用户总数: " + count);
} catch (SQLException e) {
System.err.println("获取用户总数失败: " + e.getMessage());
}
}
public static void formatName(String name) {
String sql = "{call format_name(?)}";
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
CallableStatement cstmt = conn.prepareCall(sql)) {
cstmt.setString(1, name); // 设置 IN 值
cstmt.registerOutParameter(1, Types.VARCHAR); // 注册 INOUT 参数
cstmt.execute();
String formattedName = cstmt.getString(1);
System.out.println("格式化名称 '" + name + "' -> '" + formattedName + "'");
} catch (SQLException e) {
System.err.println("格式化名称失败: " + e.getMessage());
}
}
public static void getAllUsers() {
String sql = "{call get_all_users()}";
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
CallableStatement cstmt = conn.prepareCall(sql);
ResultSet rs = cstmt.executeQuery()) { // executeQuery() 也可以用于只返回结果集的存储过程
System.out.println("\n--- 所有用户列表 ---");
while (rs.next()) {
System.out.printf("ID: %d, Name: %s, Email: %s\n",
rs.getInt("id"),
rs.getString("name"),
rs.getString("email"));
}
System.out.println("--------------------\n");
} catch (SQLException e) {
System.err.println("获取所有用户失败: " + e.getMessage());
}
}
}
希望这份详细的指南能帮助你完全掌握在 Java 中调用 MySQL 存储过程!
