Java 存储过程
在数据库编程中,存储过程是一种强大的工具,允许我们将一系列SQL语句封装在数据库服务器上执行。本文将详细介绍如何在Java应用程序中使用JDBC调用和管理数据库存储过程。
什么是存储过程?
存储过程是存储在数据库中的一组SQL语句集合,可以像调用函数一样被调用执行。它们通常用于封装复杂的业务逻辑,提高数据处理效率,并增强数据安全性。
提示
存储过程已经被预编译和优化,因此比客户端发送多个单独的SQL语句执行更高效。
存储过程的主要优势:
- 性能提升:减少网络流量,一次调用执行多条SQL语句
- 代码重用:业务逻辑可以被多个应用程序共享
- 安全性:可以限制对底层表的直接访问
- 维护简化:逻辑集中在数据库,便于维护
在Java中使用存储过程
JDBC API提供了CallableStatement
接口,专门用于执行存储过程。下面我们将一步步学习如何在Java中调用存储过程。
基本语法
// 创建调用存储过程的CallableStatement对象
CallableStatement cstmt = connection.prepareCall("{call 过程名称(?, ?, ...)}");
// 设置输入参数
cstmt.setXXX(参数索引, 参数值);
// 注册输出参数
cstmt.registerOutParameter(参数索引, 参数类型);
// 执行存储过程
cstmt.execute();
// 获取输出参数值
返回值 = cstmt.getXXX(参数索引);
存储过程参数类型
在使用存储过程时,我们通常会遇到三种类型的参数:
- IN参数:输入参数,用于向存储过程传递值
- OUT参数:输出参数,用于从存储过程获取值
- INOUT参数:既是输入又是输出的参数
实际示例
让我们通过几个实际例子来学习如何在Java中使用存储过程。
示例1:调用不带参数的存储过程
假设我们有一个简单的存储过程get_employee_count
,它返回员工总数:
CREATE PROCEDURE get_employee_count()
BEGIN
SELECT COUNT(*) FROM employees;
END
在Java中调用此存储过程:
import java.sql.*;
public class SimpleStoredProcedure {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/employees_db";
String username = "root";
String password = "password";
try (Connection conn = DriverManager.getConnection(url, username, password);
CallableStatement cstmt = conn.prepareCall("{call get_employee_count()}");
ResultSet rs = cstmt.executeQuery()) {
if (rs.next()) {
System.out.println("员工总数: " + rs.getInt(1));
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
输出示例:
员工总数: 42
示例2:使用IN参数
创建一个接收部门ID参数并返回该部门员工的存储过程:
CREATE PROCEDURE get_employees_by_dept(IN dept_id INT)
BEGIN
SELECT employee_id, first_name, last_name, salary
FROM employees
WHERE department_id = dept_id;
END
在Java中调用此存储过程:
import java.sql.*;
public class StoredProcedureWithINParam {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/employees_db";
String username = "root";
String password = "password";
try (Connection conn = DriverManager.getConnection(url, username, password);
CallableStatement cstmt = conn.prepareCall("{call get_employees_by_dept(?)}")) {
// 设置IN参数
cstmt.setInt(1, 10); // 查询部门ID为10的员工
try (ResultSet rs = cstmt.executeQuery()) {
System.out.println("部门 10 的员工列表:");
System.out.println("ID\t姓名\t\t薪资");
System.out.println("---------------------------------");
while (rs.next()) {
System.out.printf("%d\t%s %s\t%.2f\n",
rs.getInt("employee_id"),
rs.getString("first_name"),
rs.getString("last_name"),
rs.getDouble("salary"));
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
输出示例:
部门 10 的员工列表:
ID 姓名 薪资
---------------------------------
101 John Doe 5000.00
102 Jane Smith 6000.00
103 Robert Johnson 5500.00
示例3:使用OUT参数
创建一个计算并返回特定部门平均薪资的存储过程:
CREATE PROCEDURE get_avg_salary(IN dept_id INT, OUT avg_sal DECIMAL(10,2))
BEGIN
SELECT AVG(salary) INTO avg_sal
FROM employees
WHERE department_id = dept_id;
END
在Java中调用此存储过程:
import java.sql.*;
public class StoredProcedureWithOUTParam {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/employees_db";
String username = "root";
String password = "password";
try (Connection conn = DriverManager.getConnection(url, username, password);
CallableStatement cstmt = conn.prepareCall("{call get_avg_salary(?, ?)}")) {
// 设置IN参数
cstmt.setInt(1, 20); // 部门ID为20
// 注册OUT参数
cstmt.registerOutParameter(2, java.sql.Types.DECIMAL);
// 执行存储过程
cstmt.execute();
// 获取OUT参数
double avgSalary = cstmt.getDouble(2);
System.out.printf("部门 20 的平均薪资: $%.2f\n", avgSalary);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
输出示例:
部门 20 的平均薪资: $5843.75
示例4:使用INOUT参数
创建一个存储过程,接收员工ID,并返回该员工的新薪资(加10%):
CREATE PROCEDURE increase_salary(INOUT emp_id INT, INOUT sal DECIMAL(10,2))
BEGIN
SELECT salary INTO sal FROM employees WHERE employee_id = emp_id;
SET sal = sal * 1.1;
UPDATE employees SET salary = sal WHERE employee_id = emp_id;
END
在Java中调用此存储过程:
import java.sql.*;
public class StoredProcedureWithINOUTParam {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/employees_db";
String username = "root";
String password = "password";
try (Connection conn = DriverManager.getConnection(url, username, password);
CallableStatement cstmt = conn.prepareCall("{call increase_salary(?, ?)}")) {
int employeeId = 101;
// 设置INOUT参数
cstmt.setInt(1, employeeId);
cstmt.registerOutParameter(1, java.sql.Types.INTEGER);
cstmt.setDouble(2, 0); // 初始值不重要,会被存储过程覆盖
cstmt.registerOutParameter(2, java.sql.Types.DECIMAL);
// 执行存储过程
cstmt.execute();
// 获取INOUT参数的输出值
int returnedEmpId = cstmt.getInt(1);
double newSalary = cstmt.getDouble(2);
System.out.printf("员工 ID %d 的新薪资: $%.2f\n", returnedEmpId, newSalary);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
输出示例:
员工 ID 101 的新薪资: $5500.00
存储函数与存储过程的区别
存储函数是存储过程的一种特殊形式,主要区别在于:
- 存储函数必须返回一个值,而存储过程不一定
- 存储函数可以在SQL语句中使用,存储过程不行
- 存储过程可以有多个OUT参数,存储函数只能通过返回值输出
在Java中调用存储函数的示例:
import java.sql.*;
public class StoredFunction {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/employees_db";
String username = "root";
String password = "password";
try (Connection conn = DriverManager.getConnection(url, username, password);
CallableStatement cstmt = conn.prepareCall("{? = call calculate_bonus(?)}")) {
// 注册返回参数
cstmt.registerOutParameter(1, java.sql.Types.DOUBLE);
// 设置IN参数
cstmt.setInt(2, 101); // 员工ID
// 执行存储函数
cstmt.execute();
// 获取函数返回值
double bonus = cstmt.getDouble(1);
System.out.printf("员工 101 的奖金: $%.2f\n", bonus);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
存储过程的事务处理
在使用存储过程时,事务处理非常重要,可以确保数据的一致性和完整性:
import java.sql.*;
public class StoredProcedureTransaction {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/employees_db";
String username = "root";
String password = "password";
Connection conn = null;
CallableStatement cstmt = null;
try {
conn = DriverManager.getConnection(url, username, password);
// 关闭自动提交
conn.setAutoCommit(false);
// 调用转账存储过程
cstmt = conn.prepareCall("{call transfer_money(?, ?, ?)}");
cstmt.setInt(1, 101); // 转出账户
cstmt.setInt(2, 102); // 转入账户
cstmt.setDouble(3, 1000); // 转账金额
cstmt.execute();
// 提交事务
conn.commit();
System.out.println("转账成功完成");
} catch (SQLException e) {
// 发生错误时回滚事务
try {
if (conn != null) {
conn.rollback();
System.out.println("事务已回滚");
}
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
} finally {
// 关闭资源
try {
if (cstmt != null) cstmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
实际应用场景
存储过程在实际开发中有许多应用场景。以下是一些常见的使用情况:
1. 批量数据处理
当需要执行大量相似的SQL操作时,使用存储过程可以减少网络传输和提高效率。
public void processBatchEmployeeUpdates() {
try (Connection conn = dataSource.getConnection();
CallableStatement cstmt = conn.prepareCall("{call process_annual_employee_updates()}")) {
cstmt.execute();
System.out.println("年度员工信息更新完成");
} catch (SQLException e) {
e.printStackTrace();
}
}