- MySQL 端:创建存储过程
- Java 端:编写 JDBC 代码调用存储过程
- 处理不同类型的存储过程(无参数、有参数、有返回结果集)
- 最佳实践和注意事项
第1步:在 MySQL 中创建存储过程
为了演示,我们创建几个不同类型的存储过程。

简单无参存储过程 这个存储过程只是简单地返回 "Hello, World!" 字符串。
-- 删除已存在的存储过程(避免重复创建时出错)
DROP PROCEDURE IF EXISTS hello_world;
-- 创建存储过程
DELIMITER //
CREATE PROCEDURE hello_world()
BEGIN
SELECT 'Hello, World from MySQL Stored Procedure!' AS message;
END //
DELIMITER ;
带输入参数的存储过程 这个存储过程根据员工 ID 查询员工信息。
-- 假设我们有一个 employees 表
CREATE TABLE IF NOT EXISTS employees (
id INT PRIMARY KEY,
name VARCHAR(100),
department VARCHAR(100),
salary DECIMAL(10, 2)
);
INSERT INTO employees (id, name, department, salary) VALUES
(1, 'Alice', 'Engineering', 9000.00),
(2, 'Bob', 'Marketing', 7500.50),
(3, 'Charlie', 'Engineering', 11000.00);
-- 删除已存在的存储过程
DROP PROCEDURE IF EXISTS get_employee_by_id;
-- 创建存储过程
DELIMITER //
CREATE PROCEDURE get_employee_by_id(IN p_id INT)
BEGIN
SELECT id, name, department, salary
FROM employees
WHERE id = p_id;
END //
DELIMITER ;
带输出参数的存储过程 这个存储过程根据部门名称,返回该部门的平均薪资。
-- 删除已存在的存储过程
DROP PROCEDURE IF EXISTS get_avg_salary_by_dept;
-- 创建存储过程
DELIMITER //
CREATE PROCEDURE get_avg_salary_by_dept(IN p_dept_name VARCHAR(100), OUT p_avg_salary DECIMAL(10, 2))
BEGIN
SELECT AVG(salary) INTO p_avg_salary
FROM employees
WHERE department = p_dept_name;
END //
DELIMITER ;
返回结果集的存储过程
这个存储过程返回指定部门的所有员工,注意,MySQL 中直接返回结果集的存储过程不需要特殊的 OUT 参数,直接 SELECT 即可。

-- 删除已存在的存储过程
DROP PROCEDURE IF EXISTS get_employees_by_dept;
-- 创建存储过程
DELIMITER //
CREATE PROCEDURE get_employees_by_dept(IN p_dept_name VARCHAR(100))
BEGIN
SELECT id, name, department, salary
FROM employees
WHERE department = p_dept_name;
END //
DELIMITER ;
第2步:在 Java 中调用存储过程
在 Java 中,我们使用 JDBC (Java Database Connectivity) API 来调用存储过程,核心是 CallableStatement 接口。
准备工作
确保你的项目中已经添加了 MySQL JDBC 驱动的依赖。
Maven (pom.xml):
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version> <!-- 使用合适的版本 -->
</dependency>
Gradle (build.gradle):
implementation 'com.mysql:mysql-connector-j:8.0.33' // 使用合适的版本
第3步:Java 代码示例
下面我们分别调用上面创建的四种存储过程。
示例 1:调用无参、返回结果集的存储过程 (hello_world)
import java.sql.*;
public class StoredProcedureExample {
// 数据库连接信息,建议使用配置文件
private static final String DB_URL = "jdbc:mysql://localhost:3306/your_database?useSSL=false&serverTimezone=UTC";
private static final String USER = "your_username";
private static final String PASS = "your_password";
public static void main(String[] args) {
// 使用 try-with-resources 语句,确保连接、语句和结果集自动关闭
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS)) {
if (conn != null) {
System.out.println("Connected to the database successfully!");
// 1. 调用无参、返回结果集的存储过程
callSimpleStoredProcedure(conn);
// 2. 调用带输入参数的存储过程
callStoredProcedureWithInputParam(conn, 1);
// 3. 调用带输出参数的存储过程
callStoredProcedureWithOutputParam(conn, "Engineering");
// 4. 调用返回结果集的存储过程
callStoredProcedureWithResultSet(conn, "Marketing");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 调用无参、返回结果集的存储过程
*/
private static void callSimpleStoredProcedure(Connection conn) throws SQLException {
String sql = "{call hello_world()}";
try (CallableStatement cstmt = conn.prepareCall(sql)) {
// executeQuery() 用于执行返回结果集的存储过程
try (ResultSet rs = cstmt.executeQuery()) {
System.out.println("\n--- 调用 hello_world ---");
while (rs.next()) {
// 列名是在 SELECT 语句中定义的 'message'
String message = rs.getString("message");
System.out.println("Message: " + message);
}
}
}
}
}
示例 2:调用带输入参数的存储过程 (get_employee_by_id)
// ... (接上面的 StoredProcedureExample 类)
/**
* 调用带输入参数的存储过程
*/
private static void callStoredProcedureWithInputParam(Connection conn, int employeeId) throws SQLException {
// ? 是一个占位符,用于设置 IN 参数
String sql = "{call get_employee_by_id(?)}";
try (CallableStatement cstmt = conn.prepareCall(sql)) {
// 设置 IN 参数的索引 (从 1 开始) 和值
cstmt.setInt(1, employeeId);
// 执行并获取结果集
try (ResultSet rs = cstmt.executeQuery()) {
System.out.println("\n--- 调用 get_employee_by_id (ID=" + employeeId + ") ---");
while (rs.next()) {
System.out.printf("ID: %d, Name: %s, Dept: %s, Salary: %.2f%n",
rs.getInt("id"),
rs.getString("name"),
rs.getString("department"),
rs.getBigDecimal("salary"));
}
}
}
}
示例 3:调用带输出参数的存储过程 (get_avg_salary_by_dept)
这是最复杂的一种,需要使用 registerOutParameter 来注册输出参数。
// ... (接上面的 StoredProcedureExample 类)
/**
* 调用带输出参数的存储过程
*/
private static void callStoredProcedureWithOutputParam(Connection conn, String department) throws SQLException {
// 第一个 ? 是 IN 参数,第二个 ? 是 OUT 参数
String sql = "{call get_avg_salary_by_dept(?, ?)}";
try (CallableStatement cstmt = conn.prepareCall(sql)) {
// 1. 设置 IN 参数
cstmt.setString(1, department);
// 2. 注册 OUT 参数
// 参数索引 (从 1 开始)
// 参数类型 (java.sql.Types 中定义的类型)
cstmt.registerOutParameter(2, java.sql.Types.DECIMAL);
// 3. 执行存储过程
// 对于有 OUT 参数的存储过程,使用 execute() 或 executeUpdate()
cstmt.execute();
// 4. 从 OUT 参数中获取值
BigDecimal avgSalary = cstmt.getBigDecimal(2);
System.out.println("\n--- 调用 get_avg_salary_by_dept (Dept=" + department + ") ---");
System.out.println("Average Salary: " + avgSalary);
}
}
示例 4:调用返回结果集的存储过程 (get_employees_by_dept)
调用方式与示例 1 类似,只是多了一个 IN 参数。
// ... (接上面的 StoredProcedureExample 类)
/**
* 调用返回结果集的存储过程 (带 IN 参数)
*/
private static void callStoredProcedureWithResultSet(Connection conn, String department) throws SQLException {
String sql = "{call get_employees_by_dept(?)}";
try (CallableStatement cstmt = conn.prepareCall(sql)) {
// 设置 IN 参数
cstmt.setString(1, department);
// 执行并获取结果集
try (ResultSet rs = cstmt.executeQuery()) {
System.out.println("\n--- 调用 get_employees_by_dept (Dept=" + department + ") ---");
while (rs.next()) {
System.out.printf("ID: %d, Name: %s, Dept: %s, Salary: %.2f%n",
rs.getInt("id"),
rs.getString("name"),
rs.getString("department"),
rs.getBigDecimal("salary"));
}
}
}
}
}
第4步:最佳实践和注意事项
-
使用
try-with-resources- 必须始终在
try块中创建Connection,Statement,PreparedStatement,CallableStatement, 和ResultSet。 - 这样可以确保它们在使用完毕后被自动关闭,防止资源泄漏。
- 必须始终在
-
使用连接池
- 在生产环境中,绝对不要在每次请求时都创建和销毁数据库连接,这非常消耗资源。
- 应该使用连接池,如 HikariCP (目前性能最好的)、Apache DBCP 或 C3P0,它们可以复用连接,大大提高性能。
-
防止 SQL 注入
- 当调用带参数的存储过程时,使用
cstmt.setXxx(index, value)的方式来设置参数。 - 永远不要使用字符串拼接的方式来构建 SQL 调用语句,
"{call get_employee_by_id(" + employeeId + ")}",这会引入 SQL 注入风险。
- 当调用带参数的存储过程时,使用
-
处理多个结果集
- 某些存储过程可能会返回多个结果集(先返回一个状态码,再返回一个数据集)。
- 你可以使用
boolean hasResultSet = cstmt.execute();来循环处理所有结果。 cstmt.getMoreResults()方法可以移动到下一个结果。
-
清晰的代码和注释
- 为存储过程和 Java 调用代码添加清晰的注释,说明其功能、参数和返回值。
- 使用有意义的变量名。
-
错误处理
- 使用
try-catch块妥善处理SQLException,并向用户或日志记录友好的错误信息。
- 使用
通过以上步骤和示例,你应该能够掌握在 Java 中调用各种类型 MySQL 存储过程的方法。
